diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..31f554bb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[alias] +xtask = "run --package xtask --" + +[target.wasm32-unknown-unknown] +rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] diff --git a/.gitattribute b/.gitattribute new file mode 100644 index 00000000..abc99496 --- /dev/null +++ b/.gitattribute @@ -0,0 +1,8 @@ +*.rs text eol=lf +*.toml text eol=lf +*.cs text eol=lf +*.js text eol=lf +*.ps1 text eol=lf +*.sln text eol=crlf + +ffi/dotnet/Devolutions.IronRdp/Generated/** linguist-generated merge=binary \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..8b8e4f7c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# File auto-generated and managed by Devops +/.github/ @devolutions/devops @devolutions/architecture-maintainers +/.github/dependabot.yml @devolutions/security-managers diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..f21ce4fb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directories: + - "/" + - "/fuzz/" + schedule: + interval: "weekly" + assignees: + - "CBenoit" + open-pull-requests-limit: 3 + groups: + crypto: + patterns: + - "md-5" + - "md5" + - "sha1" + - "pkcs1" + - "x509-cert" + - "der" + - "*tls*" + - "*rand*" + patch: + dependency-type: "production" + update-types: + - "patch" + dev: + dependency-type: "development" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6b85c19d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,215 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +env: + # Disable incremental compilation. CI builds are often closer to from-scratch builds, as changes + # are typically bigger than from a local edit-compile cycle. + # Incremental compilation also significantly increases the amount of IO and the size of ./target + # folder, which makes caching less effective. + CARGO_INCREMENTAL: 0 + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + RUST_BACKTRACE: short + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + # Cache should never takes more than a few seconds to get downloaded. + # If it does, let’s just rebuild from scratch instead of hanging "forever". + SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 + # Disabling debug info so compilation is faster and ./target folder is smaller. + CARGO_PROFILE_DEV_DEBUG: 0 + +jobs: + formatting: + name: Check formatting + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Check formatting + run: cargo xtask check fmt -v + + typos: + name: Check typos + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Binary cache + uses: actions/cache@v4 + with: + path: ./.cargo/local_root/bin + key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }} + + - name: typos (prepare) + run: cargo xtask check install -v + + - name: typos (check) + run: cargo xtask check typos -v + + checks: + name: Checks [${{ matrix.os }}] + needs: [formatting] + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + os: [windows, linux, macos] + include: + - os: windows + runner: windows-latest + - os: linux + runner: ubuntu-latest + - os: macos + runner: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install devel packages + if: ${{ runner.os == 'Linux' }} + run: | + sudo apt-get -y install libasound2-dev + + - name: Install NASM + if: ${{ runner.os == 'Windows' }} + run: | + choco install nasm + $Env:PATH += ";$Env:ProgramFiles\NASM" + echo "PATH=$Env:PATH" >> $Env:GITHUB_ENV + shell: pwsh + + - name: Rust cache + uses: Swatinem/rust-cache@v2.7.3 + + - name: Binary cache + uses: actions/cache@v4 + with: + path: ./.cargo/local_root/bin + key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }} + + # Compilation is separated from execution so we know exactly the time for each step. + + - name: Tests (compile) + run: cargo xtask check tests --no-run -v + + - name: Tests (run) + run: cargo xtask check tests -v + + - name: Lints + run: cargo xtask check lints -v + + - name: WASM (prepare) + run: cargo xtask wasm install -v + + - name: WASM (check) + run: cargo xtask wasm check -v + + - name: Lock files + run: cargo xtask check locks -v + + fuzz: + name: Fuzzing + needs: [formatting] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Rust cache + uses: Swatinem/rust-cache@v2.7.3 + with: + workspaces: fuzz -> target + + - name: Binary cache + uses: actions/cache@v4 + with: + path: ./.cargo/local_root/bin + key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }} + + - name: Prepare + run: cargo xtask fuzz install -v + + # Simply run all fuzz targets for a few seconds, just checking there is nothing obviously wrong at a quick glance + - name: Fuzz + run: cargo xtask fuzz run -v + + - name: Lock files + run: cargo xtask check locks -v + + web: + name: Web Client + needs: [formatting] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Rust cache + uses: Swatinem/rust-cache@v2.7.3 + + - name: Binary cache + uses: actions/cache@v4 + with: + path: ./.cargo/local_root/bin + key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }} + + - name: Prepare + run: cargo xtask web install -v + + - name: Check + run: cargo xtask web check -v + + - name: Lock files + run: cargo xtask check locks -v + + ffi: + name: FFI + needs: [formatting] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Rust cache + uses: Swatinem/rust-cache@v2.7.3 + + - name: Binary cache + uses: actions/cache@v4 + with: + path: ./.cargo/local_root/bin + key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }} + + - name: Prepare runner + run: cargo xtask ffi install -v + + - name: Build native library + run: cargo xtask ffi build -v + + - name: Generate bindings + run: cargo xtask ffi bindings -v + + - name: Build .NET projects + run: cd ./ffi/dotnet && dotnet build + + success: + name: Success + if: ${{ always() }} + needs: [formatting, typos, checks, fuzz, web, ffi] + runs-on: ubuntu-latest + + steps: + - name: Check success + run: | + $results = '${{ toJSON(needs.*.result) }}' | ConvertFrom-Json + $succeeded = $($results | Where { $_ -Ne "success" }).Count -Eq 0 + exit $(if ($succeeded) { 0 } else { 1 }) + shell: pwsh diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..25aefe75 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,50 @@ +name: Coverage + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +env: + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + +jobs: + coverage: + name: Coverage Report + runs-on: ubuntu-latest + + # Running the coverage job is only supported on the official repo itself, not on forks + # (because $GITHUB_TOKEN only have read permissions when run on a fork) + # We would need something like Codecov integration to handle forks properly + # https://github.com/taiki-e/cargo-llvm-cov#continuous-integration + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' + + steps: + - uses: actions/checkout@v4 + + - name: Rust cache + uses: Swatinem/rust-cache@v2.7.3 + + - name: Prepare runner + run: cargo xtask cov install -v + + - name: Generate PR report + if: ${{ github.event.number != '' }} + run: cargo xtask cov report-gh --repo "${{ github.repository }}" --pr "${{ github.event.number }}" -v + env: + GH_TOKEN: ${{ github.token }} + + - name: Configure Git Identity + if: ${{ github.ref == 'refs/heads/master' }} + run: | + git config --local user.name "github-actions[bot]" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + + - name: Update coverage data + if: ${{ github.ref == 'refs/heads/master' }} + run: cargo xtask cov update -v + env: + GH_TOKEN: ${{ secrets.DEVOLUTIONSBOT_TOKEN }} diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 00000000..26a9d855 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,182 @@ +name: Fuzz + +on: + workflow_dispatch: + schedule: + - cron: '12 3 * * 0' # At 03:12 AM UTC on Sunday. + +env: + CARGO_INCREMENTAL: 0 + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + RUST_BACKTRACE: short + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 + +jobs: + corpus-download: + name: Download corpus + runs-on: ubuntu-latest + env: + AZURE_STORAGE_KEY: ${{ secrets.CORPUS_AZURE_STORAGE_KEY }} + + steps: + - uses: actions/checkout@v4 + + - name: Download fuzzing corpus + run: cargo xtask fuzz corpus-fetch -v + + - name: Save corpus + uses: actions/cache/save@v4 + with: + path: | + ./fuzz/corpus + ./fuzz/artifacts + key: fuzz-corpus-${{ github.run_id }} + + fuzz: + name: Fuzzing ${{ matrix.target }} + needs: [corpus-download] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: [pdu_decoding, rle_decompression, bitmap_stream, cliprdr_format, channel_processing] + + steps: + - uses: actions/checkout@v4 + + - name: Download corpus + uses: actions/cache/restore@v4 + with: + fail-on-cache-miss: true + path: | + ./fuzz/corpus + ./fuzz/artifacts + key: fuzz-corpus-${{ github.run_id }} + + - name: Print corpus + run: | + tree ./fuzz/corpus + tree ./fuzz/artifacts + + - name: Rust cache + uses: Swatinem/rust-cache@v2.7.3 + with: + workspaces: fuzz -> target + + - name: Binary cache + uses: actions/cache@v4 + with: + path: ./.cargo/local_root/bin + key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }} + + - name: Prepare runner + run: cargo xtask fuzz install -v + + - name: Fuzz + run: cargo xtask fuzz run --duration 1000 --target ${{ matrix.target }} -v + + - name: Minify fuzzing corpus + if: ${{ always() && !cancelled() }} + run: cargo xtask fuzz corpus-min --target ${{ matrix.target }} -v + + # Use GitHub artifacts instead of cache for the updated corpus + # because same cache can’t be used by multiple jobs at the same time. + # Also, we can’t dynamically create a unique cache keys for all + # the targets, because then we can’t easily retrieve this cache + # without hardcoding a step for each one. It’s not good for maintenance. + + - name: Prepare minified corpus upload + # We want to upload artifacts even if fuzzing "fails" (so we can retrieve the artifact causing the crash) + if: ${{ always() && !cancelled() }} + run: | + mkdir ${{ runner.temp }}/corpus/ + cp -r ./fuzz/corpus/${{ matrix.target }} ${{ runner.temp }}/corpus + mkdir ${{ runner.temp }}/artifacts/ + cp -r ./fuzz/artifacts/${{ matrix.target }} ${{ runner.temp }}/artifacts + + - name: Upload minified corpus + if: ${{ always() && !cancelled() }} + uses: actions/upload-artifact@v4 + with: + retention-days: 7 + name: minified-corpus-${{ matrix.target }} + path: | + ${{ runner.temp }}/corpus + ${{ runner.temp }}/artifacts + + corpus-merge: + name: Corpus merge artifacts + if: ${{ always() && !cancelled() }} + needs: [fuzz] + runs-on: ubuntu-latest + + steps: + - name: Merge Artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: minified-corpus + pattern: minified-corpus-* + delete-merged: true + + corpus-upload: + name: Upload corpus + if: ${{ always() && !cancelled() }} + needs: [corpus-merge] + runs-on: ubuntu-latest + env: + AZURE_STORAGE_KEY: ${{ secrets.CORPUS_AZURE_STORAGE_KEY }} + + steps: + - uses: actions/checkout@v4 + + - name: Download updated corpus + uses: actions/download-artifact@v4 + with: + name: minified-corpus + path: ./fuzz/ + + - name: Print corpus + run: | + tree ./fuzz/corpus + tree ./fuzz/artifacts + + - name: Upload fuzzing corpus + run: cargo xtask fuzz corpus-push -v + + - name: Clean corpus cache + run: | + curl -L \ + -X DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ github.token }}"\ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/caches?key=fuzz-corpus-${{ github.run_id }}" + + notify: + name: Notify failure + if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' }} + needs: [fuzz] + runs-on: ubuntu-latest + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_ARCHITECTURE }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + + steps: + - name: Send slack notification + id: slack + uses: slackapi/slack-github-action@v1.26.0 + with: + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*${{ github.repository }}* :warning: \n Fuzz workflow for *${{ github.repository }}* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|found a bug>" + } + } + ] + } diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 00000000..dddf8fa6 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,234 @@ +name: Publish npm package + +on: + workflow_dispatch: + inputs: + dry-run: + description: 'Dry run' + required: true + type: boolean + default: true + schedule: + - cron: '48 3 * * 1' # 3:48 AM UTC every Monday + +jobs: + preflight: + name: Preflight + runs-on: ubuntu-latest + outputs: + dry-run: ${{ steps.get-dry-run.outputs.dry-run }} + + steps: + - name: Get dry run + id: get-dry-run + run: | + $IsDryRun = '${{ github.event.inputs.dry-run }}' -Eq 'true' -Or '${{ github.event_name }}' -Eq 'schedule' + + if ($IsDryRun) { + echo "dry-run=true" >> $Env:GITHUB_OUTPUT + } else { + echo "dry-run=false" >> $Env:GITHUB_OUTPUT + } + shell: pwsh + + build: + name: Build package [${{matrix.library}}] + needs: [preflight] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + library: + - iron-remote-desktop + - iron-remote-desktop-rdp + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup wasm-pack + run: | + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + shell: bash + + - name: Install dependencies + run: | + Set-Location -Path "./web-client/${{matrix.library}}/" + npm install + shell: pwsh + + - name: Build package + run: | + Set-PSDebug -Trace 1 + + Set-Location -Path "./web-client/${{matrix.library}}/" + npm run build + Set-Location -Path ./dist + npm pack + shell: pwsh + + - name: Harvest package + run: | + Set-PSDebug -Trace 1 + + New-Item -ItemType "directory" -Path . -Name "npm-packages" + Get-ChildItem -Path ./web-client/ -Recurse *.tgz | ForEach { Copy-Item $_ "./npm-packages" } + shell: pwsh + + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: npm-${{matrix.library}} + path: npm-packages/*.tgz + + npm-merge: + name: Merge artifacts + needs: [build] + runs-on: ubuntu-latest + + steps: + - name: Merge Artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: npm + pattern: npm-* + delete-merged: true + + publish: + name: Publish package + environment: publish + if: ${{ github.event_name == 'workflow_dispatch' }} + needs: [preflight, npm-merge] + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download NPM packages artifact + uses: actions/download-artifact@v4 + with: + name: npm + path: npm-packages + + - name: Publish + run: | + Set-PSDebug -Trace 1 + + $isDryRun = '${{ needs.preflight.outputs.dry-run }}' -Eq 'true' + + $files = Get-ChildItem -Recurse npm-packages/*.tgz + + foreach ($file in $files) { + Write-Host "Processing $($file.Name)..." + + $match = [regex]::Match($file.Name, '^(?.+)-(?\d+\.\d+\.\d+)\.tgz$') + + if (-not $match.Success) { + Write-Host "Unable to parse package name/version from $($file.Name), skipping." + continue + } + + $pkgName = $match.Groups['name'].Value + + # Normalize scope for npm lookups: "devolutions-foo" => "@devolutions/foo" + if ($pkgName -like 'devolutions-*') { + $scopedName = "@devolutions/$($pkgName.Substring(12))" + } else { + $scopedName = $pkgName + } + + $pkgVersion = $match.Groups['version'].Value + + # Check if this version exists on npm; exit code 0 means it does. + npm view "$scopedName@$pkgVersion" | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Host "$scopedName@$pkgVersion already exists on npm; skipping publish." + continue + } + + $publishCmd = @('npm','publish',"$file",'--access=public') + + if ($isDryRun) { + $publishCmd += '--dry-run' + } + + $publishCmd = $publishCmd -Join ' ' + Invoke-Expression $publishCmd + } + shell: pwsh + + - name: Create version tags + if: ${{ needs.preflight.outputs.dry-run == 'false' }} + run: | + set -e + + git fetch --tags + + for file in npm-packages/*.tgz; do + base=$(basename "$file" .tgz) + + # Split base at the last hyphen to separate name and version + pkg=${base%-*} + # Strip the unscoped prefix introduced by `npm pack` for @devolutions/. + pkg=${pkg#devolutions-} + + version=${base##*-} + + tag="npm-${pkg}-v${version}" + + if git rev-parse "$tag" >/dev/null 2>&1; then + echo "Tag $tag already exists; skipping." + continue + fi + + git tag "$tag" "$GITHUB_SHA" + git push origin "$tag" + done + shell: bash + env: + GIT_AUTHOR_NAME: github-actions + GIT_AUTHOR_EMAIL: github-actions@github.com + GIT_COMMITTER_NAME: github-actions + GIT_COMMITTER_EMAIL: github-actions@github.com + + - name: Update Artifactory Cache + if: ${{ needs.preflight.outputs.dry-run == 'false' }} + run: | + gh workflow run update-artifactory-cache.yml --repo Devolutions/scheduled-tasks --field package_name="iron-remote-desktop" + gh workflow run update-artifactory-cache.yml --repo Devolutions/scheduled-tasks --field package_name="iron-remote-desktop-rdp" + env: + GH_TOKEN: ${{ secrets.DEVOLUTIONSBOT_WRITE_TOKEN }} + + notify: + name: Notify failure + if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' }} + needs: [preflight, build] + runs-on: ubuntu-latest + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_ARCHITECTURE }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + + steps: + - name: Send slack notification + id: slack + uses: slackapi/slack-github-action@v1.26.0 + with: + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*${{ github.repository }}* :fire::fire::fire::fire::fire: \n The scheduled build for *${{ github.repository }}* is <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|broken>" + } + } + ] + } diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml new file mode 100644 index 00000000..eb6350b0 --- /dev/null +++ b/.github/workflows/nuget-publish.yml @@ -0,0 +1,420 @@ +name: Publish NuGet package + +on: + workflow_dispatch: + inputs: + dry-run: + description: 'Dry run' + required: true + type: boolean + default: true + schedule: + - cron: '21 3 * * 1' # 3:21 AM UTC every Monday + +jobs: + preflight: + name: Preflight + runs-on: ubuntu-latest + outputs: + dry-run: ${{ steps.get-dry-run.outputs.dry-run }} + project-version: ${{ steps.get-version.outputs.project-version }} + package-version: ${{ steps.get-version.outputs.package-version }} + + steps: + - name: Checkout ${{ github.repository }} + uses: actions/checkout@v4 + + - name: Get dry run + id: get-dry-run + run: | + $IsDryRun = '${{ github.event.inputs.dry-run }}' -Eq 'true' -Or '${{ github.event_name }}' -Eq 'schedule' + + if ($IsDryRun) { + echo "dry-run=true" >> $Env:GITHUB_OUTPUT + } else { + echo "dry-run=false" >> $Env:GITHUB_OUTPUT + } + shell: pwsh + + - name: Get version + id: get-version + run: | + $CsprojXml = [Xml] (Get-Content .\ffi\dotnet\Devolutions.IronRdp\Devolutions.IronRdp.csproj) + $ProjectVersion = $CsprojXml.Project.PropertyGroup.Version | Select-Object -First 1 + $PackageVersion = $ProjectVersion -Replace "^(\d+)\.(\d+)\.(\d+).(\d+)$", "`$1.`$2.`$3" + echo "project-version=$ProjectVersion" >> $Env:GITHUB_OUTPUT + echo "package-version=$PackageVersion" >> $Env:GITHUB_OUTPUT + shell: pwsh + + build-native: + name: Native build + needs: [preflight] + runs-on: ${{matrix.runner}} + strategy: + fail-fast: false + matrix: + os: [win, osx, linux, ios, android] + arch: [x86, x64, arm, arm64] + include: + - os: win + runner: windows-2022 + - os: osx + runner: macos-14 + - os: linux + runner: ubuntu-22.04 + - os: ios + runner: macos-14 + - os: android + runner: ubuntu-22.04 + exclude: + - arch: arm + os: win + - arch: arm + os: osx + - arch: arm + os: linux + - arch: arm + os: ios + - arch: x86 + os: win + - arch: x86 + os: osx + - arch: x86 + os: linux + - arch: x86 + os: ios + + steps: + - name: Checkout ${{ github.repository }} + uses: actions/checkout@v4 + + - name: Configure Android NDK + if: ${{ matrix.os == 'android' }} + uses: Devolutions/actions-public/cargo-android-ndk@v1 + with: + android_api_level: "21" + + - name: Configure macOS deployement target + if: ${{ matrix.os == 'osx' }} + run: Write-Output "MACOSX_DEPLOYMENT_TARGET=10.10" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + shell: pwsh + + - name: Configure iOS deployement target + if: ${{ matrix.os == 'ios' }} + run: Write-Output "IPHONEOS_DEPLOYMENT_TARGET=12.1" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + shell: pwsh + + - name: Update runner + if: ${{ matrix.os == 'linux' }} + run: sudo apt update + + - name: Install dependencies for rustls + if: ${{ runner.os == 'Windows' }} + run: | + choco install ninja nasm + + # We need to add the NASM binary folder to the PATH manually. + # We don't need to do that for ninja. + Write-Output "PATH=$Env:PATH;$Env:ProgramFiles\NASM" >> $Env:GITHUB_ENV + + # libclang / LLVM is a requirement for AWS LC. + # https://aws.github.io/aws-lc-rs/requirements/windows.html#libclang--llvm + $VSINSTALLDIR = $(vswhere.exe -latest -requires Microsoft.VisualStudio.Component.VC.Llvm.Clang -property installationPath) + Write-Output "LIBCLANG_PATH=$VSINSTALLDIR\VC\Tools\Llvm\x64\bin" >> $Env:GITHUB_ENV + + # Install Visual Studio Developer PowerShell Module for cmdlets such as Enter-VsDevShell + Install-Module VsDevShell -Force + shell: pwsh + + # No pre-generated bindings for Android and iOS. + # https://aws.github.io/aws-lc-rs/platform_support.html#pre-generated-bindings + - name: Install bindgen-cli for aws-lc-sys + if: ${{ matrix.os == 'android' || matrix.os == 'ios' }} + run: cargo install --force --locked bindgen-cli + + # For aws-lc-sys. Error returned otherwise: + # > Unable to generate bindings: ClangDiagnostic("/usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not found\n") + - name: Install gcc-multilib + if: ${{ matrix.os == 'android' }} + run: | + sudo apt-get update + sudo apt-get install gcc-multilib + + - name: Setup LLVM + if: ${{ matrix.os == 'linux' }} + uses: Devolutions/actions-public/setup-llvm@v1 + with: + version: "18.1.8" + + - name: Setup CBake + if: ${{ matrix.os == 'linux' }} + uses: Devolutions/actions-public/setup-cbake@v1 + with: + cargo_env_scripts: true + + - name: Build native lib (${{matrix.os}}-${{matrix.arch}}) + run: | + $DotNetOs = '${{matrix.os}}' + $DotNetArch = '${{matrix.arch}}' + $DotNetRid = '${{matrix.os}}-${{matrix.arch}}' + $RustArch = @{'x64'='x86_64';'arm64'='aarch64'; + 'x86'='i686';'arm'='armv7'}[$DotNetArch] + $RustPlatform = @{'win'='pc-windows-msvc'; + 'osx'='apple-darwin';'ios'='apple-ios'; + 'linux'='unknown-linux-gnu';'android'='linux-android'}[$DotNetOs] + $LibPrefix = @{'win'='';'osx'='lib';'ios'='lib'; + 'linux'='lib';'android'='lib'}[$DotNetOs] + $LibSuffix = @{'win'='.dll';'osx'='.dylib';'ios'='.dylib'; + 'linux'='.so';'android'='.so'}[$DotNetOs] + $RustTarget = "$RustArch-$RustPlatform" + + if (($DotNetOs -eq 'android') -and ($DotNetArch -eq 'arm')) { + $RustTarget = "armv7-linux-androideabi" + } + + rustup target add $RustTarget + + if ($DotNetOs -eq 'win') { + $Env:RUSTFLAGS="-C target-feature=+crt-static" + } + + $ProjectVersion = '${{ needs.preflight.outputs.project-version }}' + $PackageVersion = '${{ needs.preflight.outputs.package-version }}' + + $CargoToml = Get-Content .\ffi\Cargo.toml + $CargoToml = $CargoToml | ForEach-Object { + if ($_.StartsWith("version =")) { "version = `"$PackageVersion`"" } else { $_ } + } + Set-Content -Path .\ffi\Cargo.toml -Value $CargoToml + + if ($DotNetOs -eq 'linux') { + $LinuxArch = @{'x64'='amd64';'arm64'='arm64'}[$DotNetArch] + $Env:SYSROOT_NAME = "ubuntu-20.04-$LinuxArch" + . "$HOME/.cargo/cbake/${RustTarget}-enter.ps1" + $Env:AWS_LC_SYS_CMAKE_BUILDER="true" + } + + $CargoParams = @( + "build", + "-p", "ffi", + "--profile", "production-ffi", + "--target", "$RustTarget" + ) + + & cargo $CargoParams + + $OutputLibraryName = "${LibPrefix}ironrdp$LibSuffix" + $RenamedLibraryName = "${LibPrefix}DevolutionsIronRdp$LibSuffix" + $OutputLibrary = Join-Path "target" $RustTarget 'production-ffi' $OutputLibraryName + $OutputPath = Join-Path "dependencies" "runtimes" $DotNetRid "native" + New-Item -ItemType Directory -Path $OutputPath | Out-Null + Copy-Item $OutputLibrary $(Join-Path $OutputPath $RenamedLibraryName) + shell: pwsh + + - name: Upload native components + uses: actions/upload-artifact@v4 + with: + name: ironrdp-${{matrix.os}}-${{matrix.arch}} + path: dependencies/runtimes/${{matrix.os}}-${{matrix.arch}} + + build-universal: + name: Universal build + needs: [preflight, build-native] + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + os: [ osx, ios ] + + steps: + - name: Checkout ${{ github.repository }} + uses: actions/checkout@v4 + + - name: Setup CCTools + uses: Devolutions/actions-public/setup-cctools@v1 + + - name: Download native components + uses: actions/download-artifact@v4 + with: + path: dependencies/runtimes + + - name: Lipo native components + run: | + Set-Location "dependencies/runtimes" + # No RID for universal binaries, see: https://github.com/dotnet/runtime/issues/53156 + $OutputPath = Join-Path "${{ matrix.os }}-universal" "native" + New-Item -ItemType Directory -Path $OutputPath | Out-Null + $Libraries = Get-ChildItem -Recurse -Path "ironrdp-${{ matrix.os }}-*" -Filter "*.dylib" | Foreach-Object { $_.FullName } | Select -Unique + $LipoCmd = $(@('lipo', '-create', '-output', (Join-Path -Path $OutputPath -ChildPath "libDevolutionsIronRdp.dylib")) + $Libraries) -Join ' ' + Write-Host $LipoCmd + Invoke-Expression $LipoCmd + shell: pwsh + + - name: Framework + if: ${{ matrix.os == 'ios' }} + run: | + $Version = '${{ needs.preflight.outputs.project-version }}' + $ShortVersion = '${{ needs.preflight.outputs.package-version }}' + $BundleName = "libDevolutionsIronRdp" + $RuntimesDir = Join-Path "dependencies" "runtimes" "ios-universal" "native" + $FrameworkDir = Join-Path "$RuntimesDir" "$BundleName.framework" + New-Item -Path $FrameworkDir -ItemType "directory" -Force + $FrameworkExecutable = Join-Path $FrameworkDir $BundleName + Copy-Item -Path (Join-Path "$RuntimesDir" "$BundleName.dylib") -Destination $FrameworkExecutable -Force + + $RPathCmd = $(@('install_name_tool', '-id', "@rpath/$BundleName.framework/$BundleName", "$FrameworkExecutable")) -Join ' ' + Write-Host $RPathCmd + Invoke-Expression $RPathCmd + + [xml] $InfoPlistXml = Get-Content (Join-Path "ffi" "dotnet" "Devolutions.IronRdp" "Info.plist") + Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleIdentifier']/following-sibling::string[1]" | + %{ + $_.Node.InnerXml = "com.devolutions.ironrdp" + } + Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleExecutable']/following-sibling::string[1]" | + %{ + $_.Node.InnerXml = $BundleName + } + Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleVersion']/following-sibling::string[1]" | + %{ + $_.Node.InnerXml = $Version + } + Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleShortVersionString']/following-sibling::string[1]" | + %{ + $_.Node.InnerXml = $ShortVersion + } + + # Write the plist *without* a BOM + $Encoding = New-Object System.Text.UTF8Encoding($false) + $Writer = New-Object System.IO.StreamWriter((Join-Path $FrameworkDir "Info.plist"), $false, $Encoding) + $InfoPlistXml.Save($Writer) + $Writer.Close() + + # .NET XML document inserts two square brackets at the end of the DOCTYPE tag + # It's perfectly valid XML, but we're dealing with plists here and dyld will not be able to read the file + ((Get-Content -Path (Join-Path $FrameworkDir "Info.plist") -Raw) -Replace 'PropertyList-1.0.dtd"\[\]', 'PropertyList-1.0.dtd"') | Set-Content -Path (Join-Path $FrameworkDir "Info.plist") + shell: pwsh + + - name: Upload native components + uses: actions/upload-artifact@v4 + with: + name: ironrdp-${{ matrix.os }}-universal + path: dependencies/runtimes/${{ matrix.os }}-universal + + build-managed: + name: Managed build + needs: [build-universal] + runs-on: windows-2022 + + steps: + - name: Check out ${{ github.repository }} + uses: actions/checkout@v4 + + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Install ios workload + run: dotnet workload install ios + + - name: Prepare dependencies + run: | + New-Item -ItemType Directory -Path "dependencies/runtimes" | Out-Null + shell: pwsh + + - name: Download native components + uses: actions/download-artifact@v4 + with: + path: dependencies/runtimes + + - name: Rename dependencies + run: | + Set-Location "dependencies/runtimes" + $(Get-Item ".\ironrdp-*") | ForEach-Object { Rename-Item $_ $_.Name.Replace("ironrdp-", "") } + Get-ChildItem * -Recurse + shell: pwsh + + - name: Build Devolutions.IronRdp (managed) + run: | + # net8.0 target packaged as Devolutions.IronRdp + dotnet build .\ffi\dotnet\Devolutions.IronRdp\Devolutions.IronRdp.csproj -c Release + # net9.0-ios target packaged as Devolutions.IronRdp.iOS + dotnet build .\ffi\dotnet\Devolutions.IronRdp\Devolutions.IronRdp.csproj -c Release /p:PackageId=Devolutions.IronRdp.iOS + shell: pwsh + + - name: Upload managed components + uses: actions/upload-artifact@v4 + with: + name: ironrdp-nupkg + path: ffi/dotnet/Devolutions.IronRdp/bin/Release/*.nupkg + + publish: + name: Publish NuGet package + environment: nuget-publish + if: ${{ needs.preflight.outputs.dry-run == 'false' }} + needs: [preflight, build-managed] + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - name: Download NuGet package artifact + uses: actions/download-artifact@v4 + with: + name: ironrdp-nupkg + path: package + + - name: NuGet login (OIDC) + uses: NuGet/login@v1 + id: nuget-login + with: + user: ${{ secrets.NUGET_BOT_USERNAME }} + + - name: Publish to nuget.org + run: | + $Files = Get-ChildItem -Recurse package/*.nupkg + + foreach ($File in $Files) { + $PushCmd = @( + 'dotnet', + 'nuget', + 'push', + "$File", + '--api-key', + '${{ steps.nuget-login.outputs.NUGET_API_KEY }}', + '--source', + 'https://api.nuget.org/v3/index.json', + '--skip-duplicate' + ) + + Write-Host "Publishing $($File.Name)..." + $PushCmd = $PushCmd -Join ' ' + Invoke-Expression $PushCmd + } + shell: pwsh + + notify: + name: Notify failure + if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' }} + needs: [preflight, build-native, build-universal, build-managed] + runs-on: ubuntu-latest + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_ARCHITECTURE }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + + steps: + - name: Send slack notification + id: slack + uses: slackapi/slack-github-action@v1.26.0 + with: + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*${{ github.repository }}* :fire::fire::fire::fire::fire: \n The scheduled build for *${{ github.repository }}* is <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|broken>" + } + } + ] + } diff --git a/.github/workflows/release-crates.yml b/.github/workflows/release-crates.yml new file mode 100644 index 00000000..96d0a78d --- /dev/null +++ b/.github/workflows/release-crates.yml @@ -0,0 +1,86 @@ +name: Release crates + +permissions: + pull-requests: write + contents: write + +on: + workflow_dispatch: + push: + branches: + - master + +jobs: + # Create a PR with the new versions and changelog, preparing the next release. + open-pr: + name: Open release PR + environment: cratesio-publish + runs-on: ubuntu-latest + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 512 + + - name: Run release-plz + id: release-plz + uses: Devolutions/actions-public/release-plz@v1 + with: + command: release-pr + git-name: Devolutions Bot + git-email: bot@devolutions.net + github-token: ${{ secrets.DEVOLUTIONSBOT_WRITE_TOKEN }} + + - name: Update fuzz/Cargo.lock + if: ${{ steps.release-plz.outputs.did-open-pr == 'true' }} + run: | + $prRaw = '${{ steps.release-plz.outputs.pr }}' + Write-Host "prRaw: $prRaw" + + $pr = $prRaw | ConvertFrom-Json + Write-Host "pr: $pr" + + Write-Host "Fetch branch $($pr.head_branch)" + git fetch origin "$($pr.head_branch)" + + Write-Host "Switch to branch $($pr.head_branch)" + git checkout "$($pr.head_branch)" + + Write-Host "Update ./fuzz/Cargo.lock" + cargo update --manifest-path ./fuzz/Cargo.toml + + Write-Host "Update last commit" + git add ./fuzz/Cargo.lock + git commit --amend --no-edit + + Write-Host "Update the release pull request" + git push --force + shell: pwsh + + # Release unpublished packages. + release: + name: Release crates + environment: cratesio-publish + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 512 + + - name: Authenticate with crates.io + id: auth + uses: rust-lang/crates-io-auth-action@v1 + + - name: Run release-plz + uses: Devolutions/actions-public/release-plz@v1 + with: + command: release + registry-token: ${{ steps.auth.outputs.token }} diff --git a/.gitignore b/.gitignore index 1e7caa9e..5156a02b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,22 @@ -Cargo.lock -target/ +# Build artifacts +/target +/dependencies +# Local cargo root +/.cargo/local_root + +# Log files +*.log + +# Coverage +/docs/coverage + +# Editor/IDE files +*~ +/tags +.idea +.vscode +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sw? diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..e28decaf --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,345 @@ +# Architecture + +This document describes the high-level architecture of IronRDP. + +> Roughly, it takes 2x more time to write a patch if you are unfamiliar with the +> project, but it takes 10x more time to figure out where you should change the +> code. + +[Source](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html) + +## Code Map + +This section talks briefly about various important directories and data structures. + +Note also which crates are **API Boundaries**. +Remember, [rules at the boundary are different](https://www.tedinski.com/2018/02/06/system-boundaries.html). + +### Core Tier + +Set of foundational libraries for which strict quality standards must be observed. +Note that all crates in this tier are **API Boundaries**. +Pay attention to the "**Architecture Invariant**" sections. + +**Architectural Invariant**: doing I/O is not allowed for these crates. + +**Architectural Invariant**: all these crates must be fuzzed. + +**Architectural Invariant**: must be `#[no_std]`-compatible (optionally using the `alloc` crate). Usage of the standard +library must be opt-in through a feature flag called `std` that is enabled by default. When the `alloc` crate is optional, +a feature flag called `alloc` must exist to enable its use. + +**Architectural Invariant**: no platform-dependant code (`#[cfg(windows)]` and such). + +**Architectural Invariant**: no non-essential dependency is allowed. + +**Architectural Invariant**: no proc-macro dependency. Dependencies such as `syn` should be pushed +as far as possible from the foundational crates so it doesn’t become too much of a compilation +bottleneck. [Compilation time is a multiplier for everything][why-care-about-build-time]. +The paper [Developer Productivity For Humans, Part 4: Build Latency, Predictability, +and Developer Productivity][developer-productivity] by Ciera Jaspan and Collin Green, Google +researchers, also elaborates on why it is important to keep build times low. + +**Architectural Invariant**: unless the performance, usability or ergonomic gain is really worth +it, the amount of [monomorphization] incurred in downstream user code should be minimal to avoid +binary bloating and to keep the compilation as parallel as possible. Large generic functions should +be avoided if possible. + +[why-care-about-build-time]: https://matklad.github.io/2021/09/04/fast-rust-builds.html#Why-Care-About-Build-Times +[developer-productivity]: https://www.computer.org/csdl/magazine/so/2023/04/10176199/1OAJyfknInm +[monomorphization]: https://rustc-dev-guide.rust-lang.org/backend/monomorph.html + +#### [`crates/ironrdp`](./crates/ironrdp) + +Meta crate re-exporting important crates. + +**Architectural Invariant**: this crate re-exports other crates and does not provide anything else. + +#### [`crates/ironrdp-core`](./crates/ironrdp-core) + +Common traits and types. + +This crate is motivated by the fact that only a few items are required to build most of the other crates such as the virtual channels. +To move up these crates up in the compilation tree, `ironrdp-core` must remain small, with very few dependencies. +It contains the most "low-context" building blocks. + +Most notable traits are `Decode` and `Encode` which are used to define a common interface for PDU encoding and decoding. +These are object-safe, and must remain so. + +Most notable types are `ReadCursor`, `WriteCursor` and `WriteBuf` which are used pervasively for encoding and decoding in a `no-std` manner. + +#### [`crates/ironrdp-pdu`](./crates/ironrdp-pdu) + +PDU encoding and decoding. + +_TODO_: clean up the dependencies + +#### [`crates/ironrdp-graphics`](./crates/ironrdp-graphics) + +Image processing primitives. + +_TODO_: break down into multiple smaller crates + +_TODO_: clean up the dependencies + +#### [`crates/ironrdp-svc`](./crates/ironrdp-svc) + +Traits to implement RDP static virtual channels. + +#### [`crates/ironrdp-dvc`](./crates/ironrdp-dvc) + +DRDYNVC static channel implementation and traits to implement dynamic virtual channels. + +#### [`crates/ironrdp-cliprdr`](./crates/ironrdp-cliprdr) + +CLIPRDR static channel for clipboard implemented as described in MS-RDPECLIP. + +#### [`crates/ironrdp-rdpdr`](./crates/ironrdp-rdpdr) + +RDPDR channel implementation. + +#### [`crates/ironrdp-rdpsnd`](./crates/ironrdp-rdpsnd) + +RDPSND static channel for audio output implemented as described in MS-RDPEA. + +#### [`crates/ironrdp-connector`](./crates/ironrdp-connector) + +State machines to drive an RDP connection sequence. + +#### [`crates/ironrdp-session`](./crates/ironrdp-session) + +State machines to drive an RDP session. + +#### [`crates/ironrdp-input`](./crates/ironrdp-input) + +Utilities to manage and build input packets. + +#### [`crates/ironrdp-rdcleanpath`](./crates/ironrdp-rdcleanpath) + +RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway. + +#### [`crates/ironrdp-error`](./crates/ironrdp-error) + +Lightweight and `no_std`-compatible generic `Error` and `Report` types. +The `Error` type wraps a custom consumer-defined type for domain-specific details (such as `PduErrorKind`). + +#### [`crates/ironrdp-propertyset`](./crates/ironrdp-propertyset) + +The main type is `PropertySet`, a key-value store for configuration options. + +#### [`crates/ironrdp-rdpfile`](./crates/ironrdp-rdpfile) + +Loader and writer for the .RDP file format. + +### Extra Tier + +Higher level libraries and binaries built on top of the core tier. +Guidelines and constraints are relaxed to some extent. + +#### [`crates/ironrdp-blocking`](./crates/ironrdp-blocking) + +Blocking I/O abstraction wrapping the state machines conveniently. + +This crate is an **API Boundary**. + +#### [`crates/ironrdp-async`](./crates/ironrdp-async) + +Provides `Future`s wrapping the state machines conveniently. + +This crate is an **API Boundary**. + +#### [`crates/ironrdp-tokio`](./crates/ironrdp-tokio) + +`Framed*` traits implementation above `tokio`’s traits. + +This crate is an **API Boundary**. + +#### [`crates/ironrdp-futures`](./crates/ironrdp-futures) + +`Framed*` traits implementation above `futures`’s traits. + +This crate is an **API Boundary**. + +#### [`crates/ironrdp-tls`](./crates/ironrdp-tls) + +TLS boilerplate common with most IronRDP clients. + +NOTE: it’s not yet clear if this crate is an API Boundary or an implementation detail for the native clients. + +#### [`crates/ironrdp-client`](./crates/ironrdp-client) + +Portable RDP client without GPU acceleration. + +#### [`crates/ironrdp-web`](./crates/ironrdp-web) + +WebAssembly high-level bindings targeting web browsers. + +This crate is an **API Boundary** (WASM module). + +#### [`web-client/iron-remote-desktop`](./web-client/iron-remote-desktop) + +Core frontend UI used by `iron-svelte-client` as a Web Component. + +This crate is an **API Boundary**. + +#### [`web-client/iron-remote-desktop-rdp`](./web-client/iron-remote-desktop-rdp) + +Implementation of the TypeScript interfaces exposed by WebAssembly bindings from `ironrdp-web` and used by `iron-svelte-client`. + +This crate is an **API Boundary**. + +#### [`web-client/iron-svelte-client`](./web-client/iron-svelte-client) + +Web-based frontend using `Svelte` and `Material` frameworks. + +#### [`crates/ironrdp-cliprdr-native`](./crates/ironrdp-cliprdr-native) + +Native CLIPRDR backend implementations. + +#### [`crates/ironrdp-cfg`](./crates/ironrdp-cfg) + +IronRDP-related utilities for ironrdp-propertyset. + +### Internal Tier + +Crates that are only used inside the IronRDP project, not meant to be published. +This is mostly test case generators, fuzzing oracles, build tools, and so on. + +**Architecture Invariant**: these crates are not, and will never be, an **API Boundary**. + +#### [`crates/ironrdp-pdu-generators`](./crates/ironrdp-pdu-generators) + +`proptest` generators for `ironrdp-pdu` types. + +#### [`crates/ironrdp-session-generators`](./crates/ironrdp-session-generators) + +`proptest` generators for `ironrdp-session` types. + +#### [`crates/ironrdp-testsuite-core`](./crates/ironrdp-testsuite-core) + +Contains all integration tests for code living in the core tier, in a single binary, organized in modules. + +**Architectural Invariant**: no dependency from another tier is allowed. It must be the case that +compiling and running the core test suite does not require building any library from the extra tier. +This is to keep iteration time short. + +#### [`crates/ironrdp-testsuite-extra`](./crates/ironrdp-testsuite-extra) + +Contains all integration tests for code living in the extra tier, in a single binary, organized in modules. + +#### [`crates/ironrdp-fuzzing`](./crates/ironrdp-fuzzing) + +Provides test case generators and oracles for use with fuzzing. + +#### [`fuzz`](./fuzz) + +Fuzz targets for code in core tier. + +#### [`xtask`](./xtask) + +IronRDP’s free-form automation using Rust code. + +### Community Tier + +Crates provided and maintained by the community. Core maintainers will not invest a lot of time into +these. One or several community maintainers are associated to each one. + +The IronRDP team is happy to accept new crates but may not necessarily commit to keeping them +working when changing foundational libraries. We promise to notify you if such a crate breaks, and +will always try to fix things when it's a minor change. + +#### [`crates/ironrdp-acceptor`](./crates/ironrdp-acceptor) (@mihneabuz) + +State machines to drive an RDP connection acceptance sequence + +#### [`crates/ironrdp-server`](./crates/ironrdp-server) (@mihneabuz) + +Extendable skeleton for implementing custom RDP servers. + +#### [`crates/ironrdp-mstsgu`](./crates/ironrdp-mstsgu) (@steffengy) + +Terminal Services Gateway Server Protocol implementation. + +#### [`crates/ironrdp-glutin-renderer`](./crates/ironrdp-glutin-renderer) (no maintainer) + +`glutin` primitives for OpenGL rendering. + +#### [`crates/ironrdp-client-glutin`](./crates/ironrdp-client-glutin) (no maintainer) + +GPU-accelerated RDP client using glutin. + +#### [`crates/ironrdp-replay-client`](./crates/ironrdp-replay-client) (no maintainer) + +Utility tool to replay RDP graphics pipeline for debugging purposes. + +## Cross-Cutting Concerns + +This section talks about the things which are everywhere and nowhere in particular. + +### General + +- Dependency injection when runtime information is necessary in core tier crates (no system call such as `gethostname`) +- Keep non-portable code out of core tier crates +- Make crate `no_std`-compatible wherever possible +- Facilitate fuzzing +- In libraries, provide concrete error types either hand-crafted or using `thiserror` crate +- In binaries, use the convenient catch-all error type `anyhow::Error` +- Free-form automation a-la `make` following [`cargo xtask`](https://github.com/matklad/cargo-xtask) specification + +### Avoid I/O wherever possible + +**Architecture Invariant**: core tier crates must never interact with the outside world. Only extra tier crates +such as `ironrdp-client`, `ironrdp-web` or `ironrdp-async` are allowed to do I/O. + +### Continuous integration + +We use GitHub action and our workflows simply run `cargo xtask`. +The expectation is that, if `cargo xtask ci` passes locally, the CI will be green as well. + +**Architecture Invariant**: `cargo xtask ci` and CI workflow must be logically equivalents. It must +be the case that a successful `cargo xtask ci` run implies a successful CI workflow run and vice versa. + +### Testing + +#### Test at the boundaries (test features, not code) + +We should focus on testing the public API of libraries (keyword: **API boundary**). +That’s why most (if not all) tests should go into the `ironrdp-testsuite-core` and `ironrdp-testsuite-extra` crates. + +#### Do not depend on external resources + +**Architecture Invariant**: tests do not depend on any kind of external resources, they are perfectly reproducible. + +#### Fuzzing + +See [`fuzz/README.md`](./fuzz/README.md). + +#### Readability + +Do not include huge binary chunks directly in source files (`*.rs`). Place these in separate files (`*.bin`, `*.bmp`) +and include them using macros such as `include_bytes!` or `include_str!`. + +#### Use `expect-test` for snapshot testing + +When comparing structured data (e.g.: error results, decoded PDUs), use `expect-test`. It is both easy to create +and maintain such tests. When something affecting the representation is changed, simply run the test again with +`UPDATE_EXPECT=1` env variable to magically update the code. + +See: + +- +- + +TODO: take further inspiration from rust-analyzer + +- https://github.com/rust-lang/rust-analyzer/blob/d7c99931d05e3723d878bea5dc26766791fa4e69/docs/dev/architecture.md#testing +- https://matklad.github.io/2021/05/31/how-to-test.html + +#### Use `rstest` for fixture-based testing + +When a test can be generalized for multiple inputs, use [`rstest`](https://github.com/la10736/rstest) to avoid code duplication. + +#### Use `proptest` for property testing + +It allows to test that certain properties of your code hold for arbitrary inputs, and if a failure +is found, automatically finds the minimal test case to reproduce the problem. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..caaee2ce --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7207 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8202ab55fcbf46ca829833f347a82a2a4ce0596f0304ac322c2d100030cd56" +dependencies = [ + "crypto-common 0.2.0-rc.4", + "inout", +] + +[[package]] +name = "aes" +version = "0.9.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e713c57c2a2b19159e7be83b9194600d7e8eb3b7c2cd67e671adf47ce189a05" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686ba04dc80c816104c96cd7782b748f6ad58c5dd4ee619ff3258cf68e83d54" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.3.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02eaa2d54d0fad0116e4b1efb65803ea0bf059ce970a67cd49718d87e807cb51" +dependencies = [ + "aes", + "const-oid 0.10.1", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.10.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.10.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "array-concat" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9067cfeb22d851858da2a5af9a82e385d363623094efa61cef7a45e651fc81" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.17", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-dnssd" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d49ffe175ab45bbfd74b548313d9d7cdfff27161a94b007b52eeeb5f9aaa15e" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-util", + "libc", + "log", + "pin-utils", + "pkg-config", + "tokio", + "winapi", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base16ct" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + +[[package]] +name = "benches" +version = "0.0.0" +dependencies = [ + "anyhow", + "async-trait", + "bytesize", + "ironrdp", + "pico-args", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.11.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9ef36a6fcdb072aa548f3da057640ec10859eb4e91ddf526ee648d50c76a949" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e59c1aab3e6c5e56afe1b7e8650be9b5a791cb997bdea449194ae62e4bf8c73" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "bmp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69985ff4f58085ac696454692d0b646a66ad1f9cc9be294c91dc51bb5df511ae" +dependencies = [ + "byteorder", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.10.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cbc" +version = "0.2.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dbf9e5b071e9de872e32b73f485e8f644ff47c7011d95476733e7482ee3e5c3" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.5.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e12a13eb01ded5d32ee9658d94f553a19e804204f2dc811df69ab4d9e0cb8c7" +dependencies = [ + "block-buffer 0.11.0-rc.5", + "crypto-common 0.2.0-rc.4", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +dependencies = [ + "bitflags 1.3.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.10.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.2", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.7.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4113edbc9f68c0a64d5b911f803eb245d04bb812680fd56776411f69c670f3e0" +dependencies = [ + "hybrid-array", + "num-traits", + "rand_core 0.9.3", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8235645834fbc6832939736ce2f2d08192652269e11010a6240f61b908a1c6" +dependencies = [ + "hybrid-array", + "rand_core 0.9.3", +] + +[[package]] +name = "crypto-mac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-primes" +version = "0.7.0-pre.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f2523fbb68811c8710829417ad488086720a6349e337c38d12fa81e09e50bf" +dependencies = [ + "crypto-bigint", + "libm", + "rand_core 0.9.3", +] + +[[package]] +name = "cryptoki" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781357a7779a8e92ea985121bbf379a9adf0777f44ab6392efc6abd5aa9b67db" +dependencies = [ + "bitflags 1.3.2", + "cryptoki-sys", + "libloading", + "log", + "paste", + "secrecy", +] + +[[package]] +name = "cryptoki-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "753e27d860277930ae9f394c119c8c70303236aab0ffab1d51f3d207dbb2bc4b" +dependencies = [ + "libloading", +] + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "ctr" +version = "0.10.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e41d01c6f73b9330177f5cf782ae5b581b5f2c7840e298e0275ceee5001434" +dependencies = [ + "cipher", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.11.0-rc.3", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "der_derive", + "flagset", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d8dd2f26c86b27a2a8ea2767ec7f9df7a89516e4794e54ac01ee618dda3aa4" +dependencies = [ + "const-oid 0.10.1", + "pem-rfc7468 1.0.0-rc.3", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "des" +version = "0.9.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f51594a70805988feb1c85495ddec0c2052e4fbe59d9c0bb7f94bfc164f4f90" +dependencies = [ + "cipher", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac89f8a64533a9b0eaa73a68e424db0fb1fd6271c74cc0125336a05f090568d" +dependencies = [ + "block-buffer 0.11.0-rc.5", + "const-oid 0.10.1", + "crypto-common 0.2.0-rc.4", + "subtle", +] + +[[package]] +name = "diplomat" +version = "0.7.0" +source = "git+https://github.com/CBenoit/diplomat?rev=6dc806e80162b6b39509a04a2835744236cd2396#6dc806e80162b6b39509a04a2835744236cd2396" +dependencies = [ + "diplomat_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diplomat-runtime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7b0f23d549a46540e26e5490cd44c64ced0d762959f1ffdec6ab0399634cf3c" + +[[package]] +name = "diplomat_core" +version = "0.7.0" +source = "git+https://github.com/CBenoit/diplomat?rev=6dc806e80162b6b39509a04a2835744236cd2396#6dc806e80162b6b39509a04a2835744236cd2396" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "serde", + "smallvec", + "strck_ident", + "syn", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dissimilar" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.17.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ab355ec063f7a110eb627471058093aba00eb7f4e70afbd15e696b79d1077b" +dependencies = [ + "der 0.8.0-rc.9", + "digest 0.11.0-rc.3", + "elliptic-curve", + "rfc6979", + "signature", + "spki 0.8.0-rc.4", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef49c0b20c0ad088893ad2a790a29c06a012b3f05bcfc66661fd22a94b32129" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.9.3", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.14.0-rc.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e3be87c458d756141f3b6ee188828132743bf90c7d14843e2835d6443e5fb03" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.11.0-rc.3", + "ff", + "group", + "hkdf", + "hybrid-array", + "once_cell", + "pem-rfc7468 1.0.0-rc.3", + "pkcs8", + "rand_core 0.9.3", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "expect-test" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0" +dependencies = [ + "dissimilar", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "ff" +version = "0.14.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d42dd26f5790eda47c1a2158ea4120e32c35ddc9a7743c98a292accc01b54ef3" +dependencies = [ + "rand_core 0.9.3", + "subtle", +] + +[[package]] +name = "ffi" +version = "0.0.0" +dependencies = [ + "anyhow", + "diplomat", + "diplomat-runtime", + "embed-resource", + "ironrdp", + "ironrdp-cliprdr-native", + "ironrdp-core", + "ironrdp-dvc-pipe-proxy", + "ironrdp-rdcleanpath", + "sspi", + "thiserror 2.0.17", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.2", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f88107cb02ed63adcc4282942e60c4d09d80208d33b360ce7c729ce6dae1739" +dependencies = [ + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "group" +version = "0.14.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff6a0b2dd4b981b1ae9e3e6830ab146771f3660d31d57bafd9018805a91b0f1" +dependencies = [ + "ff", + "rand_core 0.9.3", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "thiserror 2.0.17", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.13.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ef30358b03ca095a5b910547f4f8d4b9f163e4057669c5233ef595b1ecf008" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3fd4dc94c318c1ede8a2a48341c250d6ddecd3ba793da2820301a9f92417ad9" +dependencies = [ + "digest 0.11.0-rc.3", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.2.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1603f76010ff924b616c8f44815a42eb10fb0b93d308b41deaa8da6d4251fd4b" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "inquire" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2628910d0114e9139056161d8644a2026be7b117f8498943f9437748b04c9e0a" +dependencies = [ + "bitflags 2.10.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "iron-remote-desktop" +version = "0.7.0" +dependencies = [ + "console_error_panic_hook", + "tracing", + "tracing-subscriber", + "tracing-web", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "ironrdp" +version = "0.14.0" +dependencies = [ + "anyhow", + "async-trait", + "image", + "ironrdp-acceptor", + "ironrdp-blocking", + "ironrdp-cliprdr", + "ironrdp-cliprdr-native", + "ironrdp-connector", + "ironrdp-core", + "ironrdp-displaycontrol", + "ironrdp-dvc", + "ironrdp-graphics", + "ironrdp-input", + "ironrdp-pdu", + "ironrdp-rdpdr", + "ironrdp-rdpsnd", + "ironrdp-server", + "ironrdp-session", + "ironrdp-svc", + "opus2", + "pico-args", + "rand 0.9.2", + "sspi", + "tokio-rustls", + "tracing", + "tracing-subscriber", + "x509-cert", +] + +[[package]] +name = "ironrdp-acceptor" +version = "0.8.0" +dependencies = [ + "ironrdp-async", + "ironrdp-connector", + "ironrdp-core", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-ainput" +version = "0.4.0" +dependencies = [ + "bitflags 2.10.0", + "ironrdp-core", + "ironrdp-dvc", + "num-derive", + "num-traits", +] + +[[package]] +name = "ironrdp-async" +version = "0.8.0" +dependencies = [ + "bytes", + "ironrdp-connector", + "ironrdp-core", + "ironrdp-pdu", + "tracing", +] + +[[package]] +name = "ironrdp-bench" +version = "0.0.0" +dependencies = [ + "criterion", + "ironrdp-graphics", + "ironrdp-pdu", + "ironrdp-server", +] + +[[package]] +name = "ironrdp-blocking" +version = "0.8.0" +dependencies = [ + "bytes", + "ironrdp-connector", + "ironrdp-core", + "ironrdp-pdu", + "tracing", +] + +[[package]] +name = "ironrdp-cfg" +version = "0.1.0" +dependencies = [ + "ironrdp-propertyset", +] + +[[package]] +name = "ironrdp-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "futures-util", + "inquire", + "ironrdp", + "ironrdp-cfg", + "ironrdp-cliprdr-native", + "ironrdp-core", + "ironrdp-dvc-pipe-proxy", + "ironrdp-mstsgu", + "ironrdp-propertyset", + "ironrdp-rdcleanpath", + "ironrdp-rdpfile", + "ironrdp-rdpsnd-native", + "ironrdp-tls", + "ironrdp-tokio", + "proc-exit", + "raw-window-handle", + "semver", + "smallvec", + "softbuffer", + "tap", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tracing", + "tracing-subscriber", + "transport", + "url", + "uuid", + "whoami", + "windows 0.62.2", + "winit", + "x509-cert", +] + +[[package]] +name = "ironrdp-cliprdr" +version = "0.5.0" +dependencies = [ + "bitflags 2.10.0", + "ironrdp-core", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-cliprdr-format" +version = "0.1.4" +dependencies = [ + "ironrdp-core", + "png", +] + +[[package]] +name = "ironrdp-cliprdr-native" +version = "0.5.0" +dependencies = [ + "ironrdp-cliprdr", + "ironrdp-core", + "tracing", + "windows 0.62.2", +] + +[[package]] +name = "ironrdp-connector" +version = "0.8.0" +dependencies = [ + "arbitrary", + "ironrdp-core", + "ironrdp-error", + "ironrdp-pdu", + "ironrdp-svc", + "picky", + "picky-asn1-der", + "picky-asn1-x509", + "rand 0.9.2", + "sspi", + "tracing", + "url", +] + +[[package]] +name = "ironrdp-core" +version = "0.1.5" +dependencies = [ + "ironrdp-error", +] + +[[package]] +name = "ironrdp-displaycontrol" +version = "0.4.0" +dependencies = [ + "ironrdp-core", + "ironrdp-dvc", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-dvc" +version = "0.4.1" +dependencies = [ + "ironrdp-core", + "ironrdp-pdu", + "ironrdp-svc", + "slab", + "tracing", +] + +[[package]] +name = "ironrdp-dvc-pipe-proxy" +version = "0.2.1" +dependencies = [ + "async-trait", + "ironrdp-core", + "ironrdp-dvc", + "ironrdp-pdu", + "ironrdp-svc", + "tokio", + "tracing", +] + +[[package]] +name = "ironrdp-error" +version = "0.1.3" + +[[package]] +name = "ironrdp-futures" +version = "0.6.0" +dependencies = [ + "bytes", + "futures-util", + "ironrdp-async", +] + +[[package]] +name = "ironrdp-fuzzing" +version = "0.0.0" +dependencies = [ + "arbitrary", + "ironrdp-cliprdr", + "ironrdp-cliprdr-format", + "ironrdp-core", + "ironrdp-displaycontrol", + "ironrdp-graphics", + "ironrdp-pdu", + "ironrdp-rdpdr", + "ironrdp-rdpsnd", + "ironrdp-svc", +] + +[[package]] +name = "ironrdp-graphics" +version = "0.7.0" +dependencies = [ + "bit_field", + "bitflags 2.10.0", + "bitvec", + "bmp", + "bytemuck", + "byteorder", + "expect-test", + "ironrdp-core", + "ironrdp-pdu", + "num-derive", + "num-traits", + "yuv", +] + +[[package]] +name = "ironrdp-input" +version = "0.4.0" +dependencies = [ + "bitvec", + "ironrdp-pdu", + "smallvec", +] + +[[package]] +name = "ironrdp-mstsgu" +version = "0.0.1" +dependencies = [ + "base64", + "bitflags 2.10.0", + "futures-util", + "http-body-util", + "hyper", + "hyper-util", + "ironrdp-core", + "ironrdp-error", + "ironrdp-tls", + "log", + "tokio", + "tokio-tungstenite", + "tokio-util", + "uuid", +] + +[[package]] +name = "ironrdp-pdu" +version = "0.6.0" +dependencies = [ + "bit_field", + "bitflags 2.10.0", + "byteorder", + "der-parser", + "expect-test", + "ironrdp-core", + "ironrdp-error", + "md-5 0.10.6", + "num-bigint", + "num-derive", + "num-integer", + "num-traits", + "pkcs1 0.7.5", + "sha1 0.10.6", + "tap", + "thiserror 2.0.17", + "x509-cert", +] + +[[package]] +name = "ironrdp-pdu-generators" +version = "0.0.0" + +[[package]] +name = "ironrdp-propertyset" +version = "0.1.0" +dependencies = [ + "tracing", +] + +[[package]] +name = "ironrdp-rdcleanpath" +version = "0.2.1" +dependencies = [ + "der 0.7.10", +] + +[[package]] +name = "ironrdp-rdpdr" +version = "0.5.0" +dependencies = [ + "bitflags 2.10.0", + "ironrdp-core", + "ironrdp-error", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-rdpdr-native" +version = "0.5.0" +dependencies = [ + "ironrdp-core", + "ironrdp-pdu", + "ironrdp-rdpdr", + "ironrdp-svc", + "nix", + "tracing", +] + +[[package]] +name = "ironrdp-rdpfile" +version = "0.1.0" +dependencies = [ + "ironrdp-propertyset", +] + +[[package]] +name = "ironrdp-rdpsnd" +version = "0.6.0" +dependencies = [ + "bitflags 2.10.0", + "ironrdp-core", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-rdpsnd-native" +version = "0.4.2" +dependencies = [ + "anyhow", + "bytemuck", + "cpal", + "ironrdp-rdpsnd", + "opus2", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ironrdp-server" +version = "0.10.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "ironrdp-acceptor", + "ironrdp-ainput", + "ironrdp-async", + "ironrdp-cliprdr", + "ironrdp-core", + "ironrdp-displaycontrol", + "ironrdp-dvc", + "ironrdp-graphics", + "ironrdp-pdu", + "ironrdp-rdpsnd", + "ironrdp-svc", + "ironrdp-tokio", + "qoicoubeh", + "rayon", + "rustls-pemfile", + "tokio", + "tokio-rustls", + "tracing", + "visibility", + "x509-cert", + "zstd-safe", +] + +[[package]] +name = "ironrdp-session" +version = "0.8.0" +dependencies = [ + "ironrdp-connector", + "ironrdp-core", + "ironrdp-displaycontrol", + "ironrdp-dvc", + "ironrdp-error", + "ironrdp-graphics", + "ironrdp-pdu", + "ironrdp-svc", + "qoicoubeh", + "tracing", + "zstd-safe", +] + +[[package]] +name = "ironrdp-session-generators" +version = "0.0.0" + +[[package]] +name = "ironrdp-svc" +version = "0.5.0" +dependencies = [ + "bitflags 2.10.0", + "ironrdp-core", + "ironrdp-pdu", +] + +[[package]] +name = "ironrdp-testsuite-core" +version = "0.0.0" +dependencies = [ + "anyhow", + "array-concat", + "expect-test", + "hex", + "ironrdp-cliprdr", + "ironrdp-cliprdr-format", + "ironrdp-connector", + "ironrdp-core", + "ironrdp-displaycontrol", + "ironrdp-dvc", + "ironrdp-fuzzing", + "ironrdp-graphics", + "ironrdp-input", + "ironrdp-pdu", + "ironrdp-propertyset", + "ironrdp-rdcleanpath", + "ironrdp-rdpfile", + "ironrdp-rdpsnd", + "ironrdp-session", + "paste", + "png", + "pretty_assertions", + "proptest", + "rstest", + "visibility", +] + +[[package]] +name = "ironrdp-testsuite-extra" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "ironrdp", + "ironrdp-async", + "ironrdp-tls", + "ironrdp-tokio", + "semver", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ironrdp-tls" +version = "0.2.0" +dependencies = [ + "tokio", + "tokio-native-tls", + "tokio-rustls", + "x509-cert", +] + +[[package]] +name = "ironrdp-tokio" +version = "0.8.0" +dependencies = [ + "bytes", + "ironrdp-async", + "ironrdp-connector", + "reqwest", + "sspi", + "tokio", + "url", +] + +[[package]] +name = "ironrdp-web" +version = "0.0.0" +dependencies = [ + "anyhow", + "base64", + "chrono", + "futures-channel", + "futures-util", + "getrandom 0.2.16", + "getrandom 0.3.4", + "gloo-net", + "gloo-timers", + "iron-remote-desktop", + "ironrdp", + "ironrdp-cliprdr-format", + "ironrdp-core", + "ironrdp-futures", + "ironrdp-propertyset", + "ironrdp-rdcleanpath", + "ironrdp-rdpfile", + "js-sys", + "png", + "resize", + "rgb", + "semver", + "smallvec", + "softbuffer", + "tap", + "time", + "tracing", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "x509-cert", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "iso7816" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c7e91da489667bb054f9cd2f1c60cc2ac4478a899f403d11dbc62189215b0" +dependencies = [ + "heapless", +] + +[[package]] +name = "iso7816-tlv" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7660d28d24a831d690228a275d544654a30f3b167a8e491cf31af5fe5058b546" +dependencies = [ + "untrusted", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.2.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d546793a04a1d3049bd192856f804cfe96356e2cf36b54b4e575155babe9f41" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libopus_sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e01ac33533ea26ecd6c9479ebc44833b88b0d8e9ab046b47cad3562d29ee33" +dependencies = [ + "cmake", + "log", + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.6.0", +] + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9ec86664728010f574d67ef01aec964e6f1299241a3402857c1a8a390a62478" +dependencies = [ + "cfg-if", + "digest 0.11.0-rc.3", +] + +[[package]] +name = "md4" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da5ac363534dce5fabf69949225e174fbf111a498bf0ff794c8ea1fba9f3dda" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.10.0", + "libc", + "objc2 0.6.3", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.3", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "oid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" +dependencies = [ + "serde", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opus2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a4200c196ebf402d8a7091ce21845e8f2cd2f9c0c00deb73aaa14d024f6e6e" +dependencies = [ + "libopus_sys", +] + +[[package]] +name = "orbclient" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "247ad146e19b9437f8604c21f8652423595cf710ad108af40e77d3ae6e96b827" +dependencies = [ + "libredox", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "p256" +version = "0.14.0-pre.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b374901df34ee468167a58e2a49e468cb059868479cafebeb804f6b855423d" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.14.0-pre.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "701032b3730df6b882496d6cee8221de0ce4bc11ddc64e6d89784aa5b8a6de30" +dependencies = [ + "ecdsa", + "elliptic-curve", + "fiat-crypto", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.14.0-pre.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ba29c2906eb5c89a8c411c4f11243ee4e5517ee7d71d9a13fedc877a6057b1" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "rand_core 0.9.3", + "sha2", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.13.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3fc18bb4460ac250ba6b75dfa7cf9d0b2273e3e623f660bd6ce2c3e902342e" +dependencies = [ + "digest 0.11.0-rc.3", + "hmac", + "sha1 0.11.0-rc.2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e58fab693c712c0d4e88f8eb3087b6521d060bcaf76aeb20cb192d809115ba" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "picky" +version = "7.0.0-rc.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cdc52be663aebd70d7006ae305c87eb32a2b836d6c2f26f7e384f845d80b621" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "aes-kw", + "base64", + "block-buffer 0.11.0-rc.5", + "block-padding", + "cbc", + "cipher", + "crypto-bigint", + "crypto-common 0.2.0-rc.4", + "crypto-primes", + "ctr", + "curve25519-dalek", + "der 0.8.0-rc.9", + "des", + "digest 0.11.0-rc.3", + "ecdsa", + "ed25519", + "ed25519-dalek", + "elliptic-curve", + "ff", + "ghash", + "group", + "hex", + "hkdf", + "hmac", + "http", + "inout", + "keccak", + "md-5 0.11.0-rc.2", + "p256", + "p384", + "p521", + "pbkdf2", + "pem-rfc7468 1.0.0-rc.3", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "pkcs1 0.8.0-rc.4", + "pkcs8", + "polyval", + "primefield", + "primeorder", + "rand 0.9.2", + "rand_core 0.9.3", + "rc2", + "rfc6979", + "rsa", + "sec1", + "serde", + "serde_json", + "sha1 0.11.0-rc.2", + "sha2", + "sha3", + "signature", + "spki 0.8.0-rc.4", + "thiserror 2.0.17", + "universal-hash", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "picky-asn1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff038f9360b934342fb3c0a1d6e82c438a2624b51c3c6e3e6d7cf252b6f3ee3" +dependencies = [ + "oid", + "serde", + "serde_bytes", + "time", + "zeroize", +] + +[[package]] +name = "picky-asn1-der" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b491eb61603cba1ad5c6be0269883538f8d74136c35e3641a840fb0fbcd41efc" +dependencies = [ + "picky-asn1", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-x509" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c97cd14d567a17755910fa8718277baf39d08682a980b1b1a4b4da7d0bc61a04" +dependencies = [ + "base64", + "crypto-bigint", + "oid", + "picky-asn1", + "picky-asn1-der", + "serde", + "widestring", + "zeroize", +] + +[[package]] +name = "picky-krb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed61c8d7448649c031ecae02afb10c679524c7a9af5fb0fbee466b3cc0d6df1" +dependencies = [ + "aes", + "block-buffer 0.11.0-rc.5", + "block-padding", + "byteorder", + "cbc", + "cipher", + "crypto-bigint", + "crypto-common 0.2.0-rc.4", + "des", + "digest 0.11.0-rc.3", + "hmac", + "inout", + "oid", + "pbkdf2", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "rand 0.9.2", + "serde", + "sha1 0.11.0-rc.2", + "thiserror 2.0.17", + "uuid", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs1" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" +dependencies = [ + "der 0.8.0-rc.9", + "spki 0.8.0-rc.4", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93eac55f10aceed84769df670ea4a32d2ffad7399400d41ee1c13b1cd8e1b478" +dependencies = [ + "der 0.8.0-rc.9", + "spki 0.8.0-rc.4", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "polyval" +version = "0.7.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffd40cc99d0fbb02b4b3771346b811df94194bc103983efa0203c8893755085" +dependencies = [ + "cfg-if", + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portpicker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "primefield" +version = "0.14.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcd4a163053332fd93f39b81c133e96a98567660981654579c90a99062fbf5" +dependencies = [ + "crypto-bigint", + "ff", + "rand_core 0.9.3", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0-pre.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c36e8766fcd270fa9c665b9dc364f570695f5a59240949441b077a397f15b74" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-exit" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5390699eb9ac50677729fda96fb8339d4629f257cc6cfa6eaa673730f8f63" + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.10.0", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoicoubeh" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b82aa3fef8a980075775b8c46f874823b5b4a15de327d2dbb3b6fd818480ba" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rc2" +version = "0.9.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03621ac292cc723def9e0fd0eb9573b1df8d6a9ee7ad637fe94dfc153705f3c" +dependencies = [ + "cipher", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "reqwest" +version = "0.12.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "resize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a103d0b47e783f4579149402f7499397ab25540c7a57b2f70487a5d2d20ef0" +dependencies = [ + "rgb", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.5.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369f9c4f79388704648e7bcb92749c0d6cf4397039293a9b747694fa4fb4bae" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.10.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8955ab399f6426998fde6b76ae27233cce950705e758a6c17afd2f6d0e5d52" +dependencies = [ + "const-oid 0.10.1", + "crypto-bigint", + "crypto-primes", + "digest 0.11.0-rc.3", + "pkcs1 0.8.0-rc.4", + "pkcs8", + "rand_core 0.9.3", + "sha1 0.11.0-rc.2", + "signature", + "spki 0.8.0-rc.4", + "subtle", + "zeroize", +] + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "sec1" +version = "0.8.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dff52f6118bc9f0ac974a54a639d499ac26a6cad7a6e39bc0990c19625e793b" +dependencies = [ + "base16ct", + "der 0.8.0-rc.9", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3ef0e35b322ddfaecbc60f34ab448e157e48531288ee49fafbb053696b8ffe2" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e046edf639aa2e7afb285589e5405de2ef7e61d4b0ac1e30256e3eab911af9" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.3", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.3", +] + +[[package]] +name = "sha3" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2103ca0e6f4e9505eae906de5e5883e06fc3b2232fb5d6914890c7bbcb62f478" +dependencies = [ + "digest 0.11.0-rc.3", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc280a6ff65c79fbd6622f64d7127f32b85563bca8c53cd2e9141d6744a9056d" +dependencies = [ + "digest 0.11.0-rc.3", + "rand_core 0.9.3", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.10.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "drm", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.2", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "der 0.8.0-rc.9", +] + +[[package]] +name = "sspi" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f73fe6be958ae27fa8e982d9acc42d16f34eb74714d95bb53015528667cae4" +dependencies = [ + "async-dnssd", + "async-recursion", + "bitflags 2.10.0", + "block-buffer 0.11.0-rc.5", + "byteorder", + "cfg-if", + "crypto-bigint", + "crypto-common 0.2.0-rc.4", + "crypto-mac", + "crypto-primes", + "cryptoki", + "curve25519-dalek", + "der 0.8.0-rc.9", + "digest 0.11.0-rc.3", + "ed25519-dalek", + "ff", + "futures", + "getrandom 0.3.4", + "group", + "hickory-proto", + "hickory-resolver", + "hmac", + "md-5 0.11.0-rc.2", + "md4", + "num-derive", + "num-traits", + "oid", + "p256", + "p384", + "p521", + "pem-rfc7468 1.0.0-rc.3", + "picky", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "picky-krb", + "pkcs1 0.8.0-rc.4", + "pkcs8", + "portpicker", + "primefield", + "primeorder", + "rand 0.9.2", + "reqwest", + "rsa", + "rustls", + "rustls-native-certs", + "serde", + "sha1 0.11.0-rc.2", + "sha2", + "signature", + "spki 0.8.0-rc.4", + "time", + "tokio", + "tracing", + "url", + "uuid", + "windows 0.62.2", + "windows-registry", + "winscard", + "zeroize", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strck" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be91090ded9d8f979d9fe921777342d37e769e0b6b7296843a7a38247240e917" + +[[package]] +name = "strck_ident" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c3802b169b3858a44667f221c9a0b3136e6019936ea926fc97fbad8af77202" +dependencies = [ + "strck", + "unicode-ident", +] + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinyjson" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.9+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5238e643fc34a1d5d7e753e1532a91912d74b63b92b3ea51fde8d1b7bc79dd" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.4+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3cea6b2aa3b910092f6abd4053ea464fab5f9c170ba5e9a6aead16ec4af2b6" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.5+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c03bee5ce3696f31250db0bbaff18bc43301ce0e8db2ed1f07cbb2acf89984c" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.5+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9cd6190959dce0994aa8970cd32ab116d1851ead27e866039acaf2524ce44fa" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-web" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e6a141feebd51f8d91ebfd785af50fca223c570b86852166caa3b141defe7c" +dependencies = [ + "js-sys", + "tracing-core", + "tracing-subscriber", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "transport" +version = "0.0.0" +source = "git+https://github.com/Devolutions/devolutions-gateway?rev=06e91dfe82751a6502eaf74b6a99663f06f0236d#06e91dfe82751a6502eaf74b6a99663f06f0236d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "parking_lot", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1 0.10.6", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "universal-hash" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55be643b40a21558f44806b53ee9319595bc7ca6896372e4e08e5d7d83c9cd6" +dependencies = [ + "crypto-common 0.2.0-rc.4", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.2", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.10.0", + "rustix 1.1.2", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.10.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" +dependencies = [ + "rustix 1.1.2", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.10.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "winscard" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b6ec4e6176df62589d1ac9950f6295be87ca06ee61a7c9a579a2bcc80efe34" +dependencies = [ + "bitflags 2.10.0", + "crypto-bigint", + "flate2", + "iso7816", + "iso7816-tlv", + "num-derive", + "num-traits", + "picky", + "picky-asn1-x509", + "rand_core 0.9.3", + "rsa", + "sha1 0.11.0-rc.2", + "time", + "tracing", + "uuid", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.2", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "x25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a45998121837fd8c92655d2334aa8f3e5ef0645cdfda5b321b13760c548fd55" +dependencies = [ + "curve25519-dalek", + "rand_core 0.9.3", + "serde", + "zeroize", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", + "tls_codec", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.10.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xshell" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" + +[[package]] +name = "xtask" +version = "0.0.0" +dependencies = [ + "anyhow", + "pico-args", + "tinyjson", + "xshell", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "yuv" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f1bad143caadcfcaec93039dc9c40a30fc86f23d9e7cc03764a39fe51d9d43" +dependencies = [ + "num-traits", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 7acb0839..72904a94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,213 @@ [workspace] members = [ - "ironrdp", - "ironrdp_client" + "crates/*", + "benches", + "xtask", + "ffi", +] +resolver = "2" + +# FIXME: fix compilation +exclude = [ + "crates/ironrdp-client-glutin", + "crates/ironrdp-glutin-renderer", + "crates/ironrdp-replay-client", ] +[workspace.package] +edition = "2021" +license = "MIT OR Apache-2.0" +homepage = "https://github.com/Devolutions/IronRDP" +repository = "https://github.com/Devolutions/IronRDP" +authors = ["Devolutions Inc. ", "Teleport "] +keywords = ["rdp", "remote-desktop", "network", "client", "protocol"] +categories = ["network-programming"] + +[workspace.dependencies] +# Note that for better cross-tooling interactions, do not use workspace +# dependencies for anything that is not "workspace internal" (e.g.: mostly +# dev-dependencies). E.g.: release-plz can’t detect that a dependency has been +# updated in a way warranting a version bump in the dependant if no commit is +# touching a file associated to the crate. It is technically okay to use that +# for "private" (i.e.: not used in the public API) dependencies too, but we +# still want to make follow-up releases to stay up to date with the community, +# even for private dependencies. +expect-test = "1" +proptest = "1.4" +rstest = "0.26" + +# Note: we are trying to move away from using these crates. +# They are being kept around for now for legacy compatibility, +# but new usage should be avoided. +num-derive = "0.4" +num-traits = "0.2" + +[workspace.lints.rust] + +# == Safer unsafe == # +unsafe_op_in_unsafe_fn = "warn" +invalid_reference_casting = "warn" +unused_unsafe = "warn" +missing_unsafe_on_extern = "warn" +unsafe_attr_outside_unsafe = "warn" + +# == Correctness == # +ambiguous_negative_literals = "warn" +keyword_idents_2024 = "warn" # FIXME: remove when switched to 2024 edition + +# == Style, readability == # +elided_lifetimes_in_paths = "warn" # https://quinedot.github.io/rust-learning/dont-hide.html +absolute_paths_not_starting_with_crate = "warn" +single_use_lifetimes = "warn" +unreachable_pub = "warn" +unused_lifetimes = "warn" +unused_qualifications = "warn" +keyword_idents = "warn" +noop_method_call = "warn" +macro_use_extern_crate = "warn" +redundant_imports = "warn" +redundant_lifetimes = "warn" +trivial_numeric_casts = "warn" +# missing_docs = "warn" # TODO: NOTE(@CBenoit): we probably want to ensure this in core tier crates only + +# == Compile-time / optimization == # +unused_crate_dependencies = "warn" +unused_macro_rules = "warn" + +# == Extra-pedantic rustc == # +unit_bindings = "warn" + +[workspace.lints.clippy] + +# == Safer unsafe == # +undocumented_unsafe_blocks = "warn" +unnecessary_safety_comment = "warn" +multiple_unsafe_ops_per_block = "warn" +missing_safety_doc = "warn" +transmute_ptr_to_ptr = "warn" +as_ptr_cast_mut = "warn" +as_pointer_underscore = "warn" +cast_ptr_alignment = "warn" +fn_to_numeric_cast_any = "warn" +ptr_cast_constness = "warn" + +# == Correctness == # +as_conversions = "warn" +cast_lossless = "warn" +cast_possible_truncation = "warn" +cast_possible_wrap = "warn" +cast_sign_loss = "warn" +filetype_is_file = "warn" +float_cmp = "warn" +lossy_float_literal = "warn" +float_cmp_const = "warn" +as_underscore = "warn" +unwrap_used = "warn" +large_stack_frames = "warn" +mem_forget = "warn" +mixed_read_write_in_expression = "warn" +needless_raw_strings = "warn" +non_ascii_literal = "warn" +panic = "warn" +precedence_bits = "warn" +rc_mutex = "warn" +same_name_method = "warn" +string_slice = "warn" +suspicious_xor_used_as_pow = "warn" +unused_result_ok = "warn" +missing_panics_doc = "warn" + +# == Style, readability == # +semicolon_outside_block = "warn" # With semicolon-outside-block-ignore-multiline = true +clone_on_ref_ptr = "warn" +cloned_instead_of_copied = "warn" +pub_without_shorthand = "warn" +infinite_loop = "warn" +empty_enum_variants_with_brackets = "warn" +deref_by_slicing = "warn" +multiple_inherent_impl = "warn" +map_with_unused_argument_over_ranges = "warn" +partial_pub_fields = "warn" +trait_duplication_in_bounds = "warn" +type_repetition_in_bounds = "warn" +checked_conversions = "warn" +get_unwrap = "warn" +similar_names = "warn" # Reduce risk of confusing similar names together, and protects against typos when variable shadowing was intended. +str_to_string = "warn" +string_to_string = "warn" +std_instead_of_core = "warn" +separated_literal_suffix = "warn" +unused_self = "warn" +useless_let_if_seq = "warn" +string_add = "warn" +range_plus_one = "warn" +self_named_module_files = "warn" +# TODO: partial_pub_fields = "warn" (should we enable only in pdu crates?) +redundant_type_annotations = "warn" +unnecessary_self_imports = "warn" +try_err = "warn" +rest_pat_in_fully_bound_structs = "warn" + +# == Compile-time / optimization == # +doc_include_without_cfg = "warn" +inline_always = "warn" +large_include_file = "warn" +or_fun_call = "warn" +rc_buffer = "warn" +string_lit_chars_any = "warn" +unnecessary_box_returns = "warn" +large_futures = "warn" + +# == Extra-pedantic clippy == # +allow_attributes = "warn" +cfg_not_test = "warn" +disallowed_script_idents = "warn" +non_zero_suggestions = "warn" +renamed_function_params = "warn" +unused_trait_names = "warn" +collection_is_never_read = "warn" +copy_iterator = "warn" +expl_impl_clone_on_copy = "warn" +implicit_clone = "warn" +large_types_passed_by_value = "warn" +redundant_clone = "warn" +alloc_instead_of_core = "warn" +empty_drop = "warn" +return_self_not_must_use = "warn" +wildcard_dependencies = "warn" +wildcard_imports = "warn" + +# == Let’s not merge unintended eprint!/print! statements in libraries == # +print_stderr = "warn" +print_stdout = "warn" +dbg_macro = "warn" +todo = "warn" + +[profile.dev] +opt-level = 1 + +[profile.production] +inherits = "release" +lto = true + +[profile.production-ffi] +inherits = "release" +strip = "symbols" +codegen-units = 1 +lto = true + +[profile.production-wasm] +inherits = "release" +opt-level = "s" +lto = true + +[profile.test.package.proptest] +opt-level = 3 + +[profile.test.package.rand_chacha] +opt-level = 3 + +[patch.crates-io] +# FIXME: We need to catch up with Diplomat upstream again, but this is a significant amount of work. +# In the meantime, we use this forked version which fixes an undefined behavior in the code expanded by the bridge macro. +diplomat = { git = "https://github.com/CBenoit/diplomat", rev = "6dc806e80162b6b39509a04a2835744236cd2396" } diff --git a/README.md b/README.md index 30f90859..671cfded 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,74 @@ # IronRDP -A Rust implementation of the Microsoft Remote Desktop Protocol, with a focus on security. +[![](https://docs.rs/ironrdp/badge.svg)](https://docs.rs/ironrdp/) [![](https://img.shields.io/crates/v/ironrdp)](https://crates.io/crates/ironrdp) +A collection of Rust crates providing an implementation of the Microsoft Remote Desktop Protocol, with a focus on security. + +## Demonstration + + + +## Video Codec Support + +Supported codecs: + +- Uncompressed raw bitmap +- Interleaved Run-Length Encoding (RLE) Bitmap Codec +- RDP 6.0 Bitmap Compression +- Microsoft RemoteFX (RFX) + +## Examples + +### [`ironrdp-client`](https://github.com/Devolutions/IronRDP/tree/master/crates/ironrdp-client) + +A full-fledged RDP client based on IronRDP crates suite, and implemented using non-blocking, asynchronous I/O. + +```shell +cargo run --bin ironrdp-client -- --username --password +``` + +### [`screenshot`](https://github.com/Devolutions/IronRDP/blob/master/crates/ironrdp/examples/screenshot.rs) + +Example of utilizing IronRDP in a blocking, synchronous fashion. + +This example showcases the use of IronRDP in a blocking manner. It +demonstrates how to create a basic RDP client with just a few hundred lines +of code by leveraging the IronRDP crates suite. + +In this basic client implementation, the client establishes a connection +with the destination server, decodes incoming graphics updates, and saves the +resulting output as a BMP image file on the disk. + +```shell +cargo run --example=screenshot -- --host --username --password --output out.bmp +``` + +### How to enable RemoteFX on server + +Run the following PowerShell commands, and reboot. + +```pwsh +Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows NT\Terminal Services' -Name 'ColorDepth' -Type DWORD -Value 5 +Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows NT\Terminal Services' -Name 'fEnableVirtualizedGraphics' -Type DWORD -Value 1 +``` + +Alternatively, you may change a few group policies using `gpedit.msc`: + +1. Run `gpedit.msc`. + +2. Enable `Computer Configuration/Administrative Templates/Windows Components/Remote Desktop Services/Remote Desktop Session Host/Remote Session Environment/RemoteFX for Windows Server 2008 R2/Configure RemoteFX` + +3. Enable `Computer Configuration/Administrative Templates/Windows Components/Remote Desktop Services/Remote Desktop Session Host/Remote Session Environment/Enable RemoteFX encoding for RemoteFX clients designed for Windows Server 2008 R2 SP1` + +4. Enable `Computer Configuration/Administrative Templates/Windows Components/Remote Desktop Services/Remote Desktop Session Host/Remote Session Environment/Limit maximum color depth` + +5. Reboot. + +## Architecture + +See the [ARCHITECTURE.md](https://github.com/Devolutions/IronRDP/blob/master/ARCHITECTURE.md) document. + +## Getting help + +- Report bugs in the [issue tracker](https://github.com/Devolutions/IronRDP/issues) +- Discuss the project on the [matrix room](https://matrix.to/#/#IronRDP:matrix.org) diff --git a/STYLE.md b/STYLE.md new file mode 100644 index 00000000..85ee31d8 --- /dev/null +++ b/STYLE.md @@ -0,0 +1,623 @@ +Our approach to "clean code" is two-fold: +- we avoid blocking PRs on style changes, but +- at the same time, the codebase is constantly refactored. + +It is explicitly OK for a reviewer to flag only some nits in the PR, and then send a follow-up cleanup PR for things which are easier to explain by example, cc'ing the original author. +Sending small cleanup PRs (like renaming a single local variable) is encouraged. +These PRs are easy to merge and very welcomed. + +When reviewing pull requests prefer extending this document to leaving non-reusable comments on the pull request itself. + +# Style + +## Formatting for sizes / lengths (e.g.: in `Encode::size()` and `FIXED_PART_SIZE` definitions) + +Use an inline comment for each field of the structure. + +```rust +// GOOD +const FIXED_PART_SIZE: usize = 1 /* Version */ + 1 /* Endianness */ + 2 /* CommonHeaderLength */ + 4 /* Filler */; + +// GOOD +const FIXED_PART_SIZE: usize = 1 // Version + + 1 // Endianness + + 2 // CommonHeaderLength + + 4; // Filler + +// GOOD +fn size(&self) -> usize { + 4 // ReturnCode + + 4 // cBytes + + self.reader_names.size() // mszReaderNames + + 4 // dwState + + 4 // dwProtocol + + self.atr.len() // pbAtr + + 4 // cbAtrLen +} + +// BAD +const FIXED_PART_SIZE: usize = 1 + 1 + 2 + 4; + +// BAD +const FIXED_PART_SIZE: usize = size_of::() + size_of::() + size_of::() + size_of::(); + +// BAD +fn size(&self) -> usize { + size_of::() * 5 + self.reader_names.size() + self.atr.len() +} + +``` + +**Rationale**: boring and readable, having a comment with the name of the field is useful when following along the documentation. +Here is an excerpt illustrating this: + +![Documentation excerpt](https://user-images.githubusercontent.com/3809077/272724889-681a83c9-aa83-4f48-85f4-0721c3148508.png) + +`size_of::()` by itself is not really more useful than writing `1` directly. +The size of `u8` is not going to change, and it’s not hard to predict. +The struct also does not necessarily directly hold a `u8` as-is, and it may be hard to correlate a wrapper type with the corresponding `size_of::()`. +The memory representation of the wrapper type may differ from its network representation, so it’s not possible to always replace with `size_of::()` instead. + +## Error handling + +### Return type + +Use `crate_name::Result` (e.g.: `anyhow::Result`) rather than just `Result`. + +**Rationale:** makes it immediately clear what result that is. + +Exception: it’s not necessary when the type alias is clear enough (e.g.: `ConnectionResult`). + +### Formatting of error messages + +A single sentence which: +- is short and concise, +- does not start by a capital letter, and +- does not contain trailing punctuation. + +This is the convention adopted by the Rust project: +- [Rust API Guidelines][api-guidelines-errors] +- [std::error::Error][std-error-trait] + +Also, use proper abbreviation casing, e.g., IPv4 and IPv6 (not ipv4/ipv6). + +```rust +// GOOD +"invalid X.509 certificate" + +// BAD +"Invalid X.509 certificate." +``` + +**Rationale**: it’s easier to compose with other error messages. + +To illustrate with terminal error reports: +``` +// GOOD +Error: invalid server license, caused by invalid X.509 certificate, caused by unexpected ASN.1 DER tag: expected SEQUENCE, got CONTEXT-SPECIFIC [19] (primitive) + +// BAD +Error: Invalid server license., Caused by Invalid X.509 certificate., Caused by Unexpected ASN.1 DER tag: expected SEQUENCE, got CONTEXT-SPECIFIC [19] (primitive) +``` + +The error reporter (e.g.: `ironrdp_error::ErrorReport`) is responsible for adding the punctuation and/or capitalizing the text down the line. + +[api-guidelines-errors]: https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err +[std-error-trait]: https://doc.rust-lang.org/stable/std/error/trait.Error.html + +## Logging + +If any, the human-readable message should start with a capital letter and not end with a period. + +```rust +// GOOD +info!("Connect to RDP host"); + +// BAD +info!("connect to RDP host."); +``` + +**Rationale**: consistency. +Log messages are typically not composed together like error messages, so it’s fine to start with a capital letter. + +Use tracing ability to [record structured fields][tracing-fields]. + +```rust +// GOOD +info!(%server_addr, "Looked up server address"); + +// BAD +info!("Looked up server address: {server_addr}"); +``` + +**Rationale**: structured diagnostic information is tracing’s strength. +It’s possible to retrieve the records emitted by tracing in a structured manner. + +Name fields after what already exist consistently as much as possible. +For example, errors are typically recorded as fields named `error`. + +```rust +// GOOD +error!(?error, "Active stage failed"); +error!(error = ?e, "Active stage failed"); +error!(%error, "Active stage failed"); +error!(error = format!("{err:#}"), "Active stage failed"); + +// BAD +error!(?e, "Active stage failed"); +error!(%err, "Active stage failed"); +``` + +**Rationale**: consistency. +We can rely on this to filter and collect diagnostics. + +[tracing-fields]: https://docs.rs/tracing/latest/tracing/index.html#recording-fields + +## Helper functions + +Avoid creating single-use helper functions: + +```rust +// GOOD +let buf = { + let mut buf = WriteBuf::new(); + buf.write_u32(42); + buf +}; + +// BAD +let buf = prepare_buf(42); + +// Somewhere else +fn prepare_buf(value: u32) -> WriteBuf { + let mut buf = WriteBuf::new(); + buf.write_u32(value); + buf +} +``` + +**Rationale:** single-use functions change frequently, adding or removing parameters adds churn. +A block serves just as well to delineate a bit of logic, but has access to all the context. +Re-using originally single-purpose function often leads to bad coupling. + +Exception: if you want to make use of `return` or `?`. + +## Local helper functions + +Put nested helper functions at the end of the enclosing functions (this requires using return statement). +Don't nest more than one level deep. + +```rust +// GOOD +fn func() -> u32 { + return helper(); + + fn helper() -> u32 { + /* ... */ + } +} + +// BAD +fn func() -> u32 { + fn helper() -> u32 { + /* ... */ + } + + helper() +} +``` + +**Rationale:** consistency, improved top-down readability. + +## Documentation + +### Doc comments should link to reference documents + +Add links to specification and/or other relevant documents in doc comments. +Include verbatim the name of the section or the description of the item from the specification. +Use reference-style links for readability. +Do not make the link too long. + +```rust +// GOOD + +/// [2.2.3.3.8] Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ) +/// +/// The server issues a query information request on a redirected file system device. +/// +/// [2.2.3.3.8]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e43dcd68-2980-40a9-9238-344b6cf94946 +pub struct ServerDriveQueryInformationRequest { + /* snip */ +} + +// BAD (no doc comment) + +pub struct ServerDriveQueryInformationRequest { + /* snip */ +} + +// BAD (non reference-style links make barely readable, very long lines) + +/// [2.2.3.3.8](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e43dcd68-2980-40a9-9238-344b6cf94946) Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ) +/// +/// The server issues a query information request on a redirected file system device. +pub struct ServerDriveQueryInformationRequest { + /* snip */ +} + +// BAD (long link) + +/// [2.2.3.3.8 Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ)] +/// +/// The server issues a query information request on a redirected file system device. +/// +/// [2.2.3.3.8 Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ)]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e43dcd68-2980-40a9-9238-344b6cf94946 +pub struct ServerDriveQueryInformationRequest { + /* snip */ +} +``` + +**Rationale**: consistency. +Easy cross-referencing between code and reference documents. + +### Inline code comments are proper sentences + +Style inline code comments as proper sentences. +Start with a capital letter, end with a dot. + +```rust +// GOOD + +// When building a library, `-` in the artifact name are replaced by `_`. +let artifact_name = format!("{}.wasm", package.replace('-', "_")); + +// BAD + +// when building a library, `-` in the artifact name are replaced by `_` +let artifact_name = format!("{}.wasm", package.replace('-', "_")); +``` + +**Rationale:** writing a sentence (or maybe even a paragraph) rather just "a comment" creates a more appropriate frame of mind. +It tricks you into writing down more of the context you keep in your head while coding. + +Exception: no period for brief comments (e.g., `// VER`, `// RSV`, `// ATYP`) + +### "Sentence per line" style + +For `.md` and `.adoc` files, prefer a sentence-per-line format, don't wrap lines. +If the line is too long, you want to split the sentence in two. + +**Rationale:** much easier to edit the text and read the diff, see [this link][asciidoctor-practices]. + +[asciidoctor-practices]: https://asciidoctor.org/docs/asciidoc-recommended-practices/#one-sentence-per-line + +## Invariants + +Recommended reads: + +- +- +- +- +- + +### Write down invariants clearly + +Write down invariants using `INVARIANT:` code comments. + +```rust +// GOOD + +// INVARIANT: for i in 0..lo: xs[i] < x + +// BAD + +// for i in 0..lo: xs[i] < x +``` + +**Rationale**: invariants should be upheld at all times. +It’s useful to keep invariants in mind when analyzing the flow of the code. +It’s easy to look up the local invariants when programming "in the small". + +For field invariants, a doc comment should come at the place where they are declared, inside the type definition. + +```rust +// GOOD +struct BitmapInfoHeader { + /// INVARIANT: `width.abs() <= u16::MAX` + width: i32, +} + +// BAD + +/// INVARIANT: `width.abs() <= u16::MAX` +struct BitmapInfoHeader { + width: i32, +} + +// BAD +struct BitmapInfoHeader { + width: i32, +} + +impl BitmapInfoHeader { + fn new(width: i32) -> Option { + // INVARIANT: width.abs() <= u16::MAX + if !(width.abs() <= i32::from(u16::MAX)) { + return None; + } + + Some(BitmapInfoHeader { width }) + } +} +``` + +**Rationale**: it’s easy to find about the invariant. +The invariant will show up in the documentation (typically available by hovering the item in IDEs). + +For loop invariants, the comment should come before or at the beginning of the loop. + +```rust +// GOOD + +/// Computes the smallest index such that, if `x` is inserted at this index, the array remains sorted. +fn insertion_point(xs: &[i32], x: i32) -> usize { + let mut lo = 0; + let mut hi = xs.len(); + + while lo < hi { + // INVARIANT: for i in 0..lo: xs[i] < x + // INVARIANT: for i in hi..: x <= xs[i] + + let mid = lo + (hi - lo) / 2; + if xs[mid] < x { + lo = mid + 1; + } else { + hi = mid; + } + } + + lo +} + +// BAD +fn insertion_point(xs: &[i32], x: i32) -> usize { + let mut lo = 0; + let mut hi = xs.len(); + + while lo < hi { + let mid = lo + (hi - lo) / 2; + if xs[mid] < x { + lo = mid + 1; + } else { + hi = mid; + } + } + + // INVARIANT: for i in 0..lo: xs[i] < x + // INVARIANT: for i in hi..: x <= xs[i] + + lo +} +``` + +**Rationale**: improved top-down readability, only read forward, no need to backtrack. + +For function output invariants, the comment should be specified in the doc comment. +(However, consider [enforcing this invariant][parse-dont-validate] using [the type system][type-safety] instead.) + +```rust +// GOOD + +/// Computes the stride of an uncompressed RGB bitmap. +/// +/// INVARIANT: `width <= output (stride) <= width * 4` +fn rgb_bmp_stride(width: u16, bit_count: u16) -> usize { + assert!(bit_count <= 32); + let stride = /* ... */; + stride +} + +// BAD + +/// Computes the stride of an uncompressed RGB bitmap. +fn rgb_bmp_stride(width: u16, bit_count: u16) -> usize { + assert!(bit_count <= 32); + // INVARIANT: width <= stride <= width * 4 + let stride = /* ... */; + stride +} +``` + +**Rationale**: it’s easy to find about the invariant. +The invariant will show up in the documentation (typically available by hovering the item in IDEs). + +[parse-dont-validate]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ +[type-safety]: https://www.parsonsmatt.org/2017/10/11/type_safety_back_and_forth.html + +### Explain non-obvious assumptions by referencing the invariants + +Explain clearly non-obvious assumptions and invariants relied upon (e.g.: when disabling a lint locally). +When referencing invariants, do not use the `INVARIANT:` comment prefix which is reserved for defining them. + +```rust +// GOOD + +// Per invariants: width * dst_n_samples <= 10_000 * 4 < usize::MAX +#[allow(clippy::arithmetic_side_effects)] +let dst_stride = usize::from(width) * dst_n_samples; + +// BAD +#[allow(clippy::arithmetic_side_effects)] +let dst_stride = usize::from(width) * dst_n_samples; + +// BAD + +// INVARIANT: width * dst_n_samples <= 10_000 * 4 < usize::MAX +#[allow(clippy::arithmetic_side_effects)] +let dst_stride = usize::from(width) * dst_n_samples; +``` + +**Rationale**: make the assumption obvious. +The code is easier to review. +No one will lose time refactoring based on the wrong assumption. + +### State invariants positively + +Establish invariants positively. +Prefer `if !invariant` to `if negated_invariant`. + +```rust +// GOOD +if !(idx < len) { + return None; +} + +// GOOD +check_invariant(idx < len)?; + +// GOOD +ensure!(idx < len); + +// GOOD +debug_assert!(idx < len); + +// GOOD +if idx < len { + /* ... */ +} else { + return None; +} + +// BAD +if idx >= len { + return None; +} +``` + +**Rationale:** it's useful to see the invariant relied upon by the rest of the function clearly spelled out. + +### Strongly prefer `<` and `<=` over `>` and `>=` + +Use `<` and `<=` operators instead of `>` and `>=`. + +```rust +/// GOOD +if lo <= x && x <= hi {} +if x < lo || hi < x {} + +/// BAD +if x >= lo && x <= hi {} +if x < lo || x > hi {} +``` + +**Rationale**: consistent, canonicalized form that is trivial to visualize by reading from left to right. +Things are naturally ordered from small to big like in the [number line]. + +[number line]: https://en.wikipedia.org/wiki/Number_line + +## Context parameters + +Some parameters are threaded unchanged through many function calls. +They determine the "context" of the operation. +Pass such parameters first, not last. +If there are several context parameters, consider [packing them into a `struct Ctx` and passing it as `&self`][ra-ctx-struct]. + +```rust +// GOOD +fn do_something(connector: &mut ClientConnector, certificate: &[u8]) { + let public_key = extract_public_key(certificate); + do_something_else(connector, public_key, |kind| /* … */); +} + +fn do_something_else(connector: &mut ClientConnector, public_key: &[u8], op: impl Fn(KeyKind) -> bool) { + /* ... */ +} + +// BAD +fn do_something(certificate: &[u8], connector: &mut ClientConnector) { + let public_key = extract_public_key(certificate); + do_something_else(|kind| /* … */, connector, public_key); +} + +fn do_something_else(op: impl Fn(KeyKind) -> bool, connector: &mut ClientConnector, public_key: &[u8]) { + /* ... */ +} +``` + +**Rationale:** consistency. +Context-first works better when non-context parameter is a lambda. + +[ra-ctx-struct]: https://github.com/rust-lang/rust-analyzer/blob/76633199f4316b9c659d4ec0c102774d693cd940/crates/ide-db/src/path_transform.rs#L192-L339 + +# Runtime and compile time performance + +## Avoid allocations + +Avoid writing code which is slower than it needs to be. +Don't allocate a `Vec` where an iterator would do, don't allocate strings needlessly. + +```rust +// GOOD +let second_word = text.split(' ').nth(1)?; + +// BAD +let words: Vec<&str> = text.split(' ').collect(); +let second_word = words.get(1)?; +``` + +**Rationale:** not allocating is almost always faster. + +## Push allocations to the call site + +If allocation is inevitable, let the caller allocate the resource: + +```rust +// GOOD +fn frobnicate(s: String) { + /* snip */ +} + +// BAD +fn frobnicate(s: &str) { + let s = s.to_string(); + /* snip */ +} +``` + +**Rationale:** reveals the costs. +It is also more efficient when the caller already owns the allocation. + +## Avoid monomorphization + +Avoid making a lot of code type parametric, *especially* on the boundaries between crates. + +```rust +// GOOD +fn frobnicate(f: impl FnMut()) { + frobnicate_impl(&mut f) +} +fn frobnicate_impl(f: &mut dyn FnMut()) { + /* lots of code */ +} + +// BAD +fn frobnicate(f: impl FnMut()) { + /* lots of code */ +} +``` + +Avoid `AsRef` polymorphism, it pays back only for widely used libraries: + +```rust +// GOOD +fn frobnicate(f: &Path) { } + +// BAD +fn frobnicate(f: impl AsRef) { } +``` + +**Rationale:** Rust uses monomorphization to compile generic code, meaning that for each instantiation of a generic functions with concrete types, the function is compiled afresh, *per crate*. +This allows for fantastic performance, but leads to increased compile times. +Runtime performance obeys the 80/20 rule (Pareto Principle) — only a small fraction of code is hot. +Compile time **does not** obey this rule — all code has to be compiled. diff --git a/benches/Cargo.toml b/benches/Cargo.toml new file mode 100644 index 00000000..19a87c6f --- /dev/null +++ b/benches/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "benches" +version = "0.0.0" +description = "IronRDP benchmarks" +publish = false +edition.workspace = true + +[[bin]] +name = "perfenc" +path = "src/perfenc.rs" + +[features] +default = ["qoi", "qoiz"] +qoi = ["ironrdp/qoi"] +qoiz = ["ironrdp/qoiz"] + +[dependencies] +anyhow = "1.0.99" +async-trait = "0.1.89" +bytesize = "2.3" +ironrdp = { path = "../crates/ironrdp", features = [ + "server", + "pdu", + "__bench", +] } +pico-args = "0.5.0" +tokio = { version = "1", features = ["sync", "fs", "time"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing = { version = "0.1", features = ["log"] } + +[lints] +workspace = true diff --git a/benches/src/perfenc.rs b/benches/src/perfenc.rs new file mode 100644 index 00000000..5abc94f3 --- /dev/null +++ b/benches/src/perfenc.rs @@ -0,0 +1,210 @@ +#![allow(unused_crate_dependencies)] // False positives because there are both a library and a binary. +#![allow(clippy::print_stderr)] +#![allow(clippy::print_stdout)] + +use core::num::{NonZeroU16, NonZeroUsize}; +use core::time::Duration; +use std::io::Write as _; +use std::time::Instant; + +use anyhow::Context as _; +use ironrdp::pdu::rdp::capability_sets::{CmdFlags, EntropyBits}; +use ironrdp::server::bench::encoder::{UpdateEncoder, UpdateEncoderCodecs}; +use ironrdp::server::{BitmapUpdate, DesktopSize, DisplayUpdate, PixelFormat, RdpServerDisplayUpdates}; +use tokio::fs::File; +use tokio::io::AsyncReadExt as _; +use tokio::time::sleep; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), anyhow::Error> { + setup_logging()?; + let mut args = pico_args::Arguments::from_env(); + + if args.contains(["-h", "--help"]) { + println!("Usage: perfenc [OPTIONS] "); + println!(); + println!("Measure the performance of the IronRDP server encoder, given a raw RGBX video input file."); + println!(); + println!("Options:"); + println!(" --width Width of the display (default: 3840)"); + println!(" --height Height of the display (default: 2400)"); + println!(" --codec Codec to use (default: remotefx)"); + println!(" Valid values: qoi, qoiz, remotefx, bitmap, none"); + println!(" --fps Frames per second (default: none)"); + std::process::exit(0); + } + + let width = args.opt_value_from_str("--width")?.unwrap_or(3840); + let height = args.opt_value_from_str("--height")?.unwrap_or(2400); + let codec = args.opt_value_from_str("--codec")?.unwrap_or_else(OptCodec::default); + let fps = args.opt_value_from_str("--fps")?.unwrap_or(0); + + let filename: String = args.free_from_str().context("missing RGBX input filename")?; + let file = File::open(&filename) + .await + .with_context(|| format!("Failed to open file: {filename}"))?; + + let mut flags = CmdFlags::all(); + let mut update_codecs = UpdateEncoderCodecs::new(); + + match codec { + OptCodec::RemoteFX => update_codecs.set_remotefx(Some((EntropyBits::Rlgr3, 0))), + OptCodec::Bitmap => { + flags -= CmdFlags::SET_SURFACE_BITS; + } + OptCodec::None => {} + #[cfg(feature = "qoi")] + OptCodec::Qoi => update_codecs.set_qoi(Some(0)), + #[cfg(feature = "qoiz")] + OptCodec::QoiZ => update_codecs.set_qoiz(Some(0)), + }; + + let mut encoder = UpdateEncoder::new(DesktopSize { width, height }, flags, update_codecs) + .context("failed to initialize update encoder")?; + + let mut total_raw = 0u64; + let mut total_enc = 0u64; + let mut n_updates = 0u64; + let mut updates = DisplayUpdates::new(file, DesktopSize { width, height }, fps); + while let Some(up) = updates.next_update().await? { + if let DisplayUpdate::Bitmap(ref up) = up { + total_raw += u64::try_from(up.data.len())?; + } else { + eprintln!("Invalid update"); + break; + } + let mut iter = encoder.update(up); + loop { + let Some(frag) = iter.next().await else { + break; + }; + let len = u64::try_from(frag?.data.len())?; + total_enc += len; + } + n_updates += 1; + print!("."); + std::io::stdout().flush()?; + } + println!(); + + #[expect(clippy::as_conversions, reason = "casting u64 to f64")] + let ratio = total_enc as f64 / total_raw as f64; + let percent = 100.0 - ratio * 100.0; + println!("Encoder: {encoder:?}"); + println!("Nb updates: {n_updates:?}"); + println!( + "Sum of bytes: {}/{} ({:.2}%)", + bytesize::ByteSize(total_enc), + bytesize::ByteSize(total_raw), + percent, + ); + Ok(()) +} + +struct DisplayUpdates { + file: File, + desktop_size: DesktopSize, + fps: u64, + last_update_time: Option, +} + +impl DisplayUpdates { + fn new(file: File, desktop_size: DesktopSize, fps: u64) -> Self { + Self { + file, + desktop_size, + fps, + last_update_time: None, + } + } +} + +#[async_trait::async_trait] +impl RdpServerDisplayUpdates for DisplayUpdates { + async fn next_update(&mut self) -> anyhow::Result> { + let stride = self.desktop_size.width as usize * 4; + let frame_size = stride * self.desktop_size.height as usize; + let mut buf = vec![0u8; frame_size]; + // FIXME: AsyncReadExt::read_exact is not cancellation safe. + self.file.read_exact(&mut buf).await.context("read exact")?; + + let now = Instant::now(); + if let Some(last_update_time) = self.last_update_time { + let elapsed = now - last_update_time; + if self.fps > 0 && elapsed < Duration::from_millis(1000 / self.fps) { + sleep(Duration::from_millis( + 1000 / self.fps + - u64::try_from(elapsed.as_millis()) + .context("invalid `elapsed millis`: out of range integral conversion")?, + )) + .await; + } + } + self.last_update_time = Some(now); + + let up = DisplayUpdate::Bitmap(BitmapUpdate { + x: 0, + y: 0, + width: NonZeroU16::new(self.desktop_size.width).context("width cannot be zero")?, + height: NonZeroU16::new(self.desktop_size.height).context("height cannot be zero")?, + format: PixelFormat::RgbX32, + data: buf.into(), + stride: NonZeroUsize::new(stride).context("stride cannot be zero")?, + }); + Ok(Some(up)) + } +} + +fn setup_logging() -> anyhow::Result<()> { + use tracing::metadata::LevelFilter; + use tracing_subscriber::prelude::*; + use tracing_subscriber::EnvFilter; + + let fmt_layer = tracing_subscriber::fmt::layer().compact(); + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .with_env_var("IRONRDP_LOG") + .from_env_lossy(); + + tracing_subscriber::registry() + .with(fmt_layer) + .with(env_filter) + .try_init() + .context("failed to set tracing global subscriber")?; + + Ok(()) +} + +enum OptCodec { + RemoteFX, + Bitmap, + None, + #[cfg(feature = "qoi")] + Qoi, + #[cfg(feature = "qoiz")] + QoiZ, +} + +impl Default for OptCodec { + fn default() -> Self { + Self::RemoteFX + } +} + +impl core::str::FromStr for OptCodec { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "remotefx" => Ok(Self::RemoteFX), + "bitmap" => Ok(Self::Bitmap), + "none" => Ok(Self::None), + #[cfg(feature = "qoi")] + "qoi" => Ok(Self::Qoi), + #[cfg(feature = "qoiz")] + "qoiz" => Ok(Self::QoiZ), + _ => anyhow::bail!("unknown codec: {s}"), + } + } +} diff --git a/ci/build.sh b/ci/build.sh deleted file mode 100644 index 314cc6ce..00000000 --- a/ci/build.sh +++ /dev/null @@ -1,13 +0,0 @@ -set -ex - -cargo fmt --all -- --check -cargo clippy --all-targets --all-features -- -D warnings - -cargo build -cargo build --release - -cargo build --all --exclude=ironrdp_client --target wasm32-unknown-unknown -cargo build --all --exclude=ironrdp_client --target wasm32-unknown-unknown --release - -cargo test -cargo test --release diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..99bb98be --- /dev/null +++ b/cliff.toml @@ -0,0 +1,94 @@ +# Configuration file for git-cliff + +[changelog] +trim = false + +header = """ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +""" + +# https://tera.netlify.app/docs/#introduction +body = """ +{% if version -%} + ## [[{{ version | trim_start_matches(pat="v") }}]{%- if release_link -%}({{ release_link }}){% endif %}] - {{ timestamp | date(format="%Y-%m-%d") }} +{%- else -%} + ## [Unreleased] +{%- endif %} + +{% for group, commits in commits | group_by(attribute="group") -%} + +### {{ group | upper_first }} + +{%- for commit in commits %} + +{%- set message = commit.message | upper_first %} + +{%- if commit.breaking %} + {%- set breaking = "[**breaking**] " %} +{%- else %} + {%- set breaking = "" %} +{%- endif %} + +{%- set short_sha = commit.id | truncate(length=10, end="") %} +{%- set commit_url = "https://github.com/Devolutions/IronRDP/commit/" ~ commit.id %} +{%- set commit_link = "[" ~ short_sha ~ "](" ~ commit_url ~ ")" %} + +- {{ breaking }}{{ message }} ({{ commit_link }}) \ + {% if commit.body %}\n\n {{ commit.body | replace(from="\n", to="\n ") }}{% endif %} +{%- endfor %} + +{% endfor -%} +""" + +footer = "" + +[git] +conventional_commits = true +filter_unconventional = false +filter_commits = false +date_order = false +protect_breaking_commits = true +sort_commits = "oldest" + +commit_preprocessors = [ + # Replace the issue number with the link. + { pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/Devolutions/IronRDP/issues/${1}))" }, + # Replace commit sha1 with the link. + { pattern = '([a-f0-9]{10})([a-f0-9]{30})', replace = "[${0}](https://github.com/Devolutions/IronRDP/commit/${1}${2})" }, +] + +# regex for parsing and grouping commits +# is a trick to control the section order: https://github.com/orhun/git-cliff/issues/9#issuecomment-914521594 +commit_parsers = [ + { message = "^chore", skip = true }, + { message = "^style", skip = true }, + { message = "^refactor", skip = true }, + { message = "^test", skip = true }, + { message = "^ci", skip = true }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { footer = "^[Cc]hangelog: ?ignore", skip = true }, + + { message = "(?i)security", group = "Security" }, + { body = "(?i)security", group = "Security" }, + { footer = "^[Ss]ecurity: ?yes", group = "Security" }, + + { message = "^feat", group = "Features" }, + + { message = "^revert", group = "Revert" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^doc", group = "Documentation" }, + { message = "^build", group = "Build" }, + + { message = "(?i)improve", group = "Improvements" }, + { message = "(?i)adjust", group = "Improvements" }, + { message = "(?i)change", group = "Improvements" }, + + { message = ".*", group = "Please Sort" }, +] diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..17e7e90f --- /dev/null +++ b/clippy.toml @@ -0,0 +1,6 @@ +msrv = "1.87" +semicolon-outside-block-ignore-multiline = true +accept-comment-above-statement = true +accept-comment-above-attributes = true +allow-panic-in-tests = true +allow-unwrap-in-tests = true diff --git a/crates/iron-remote-desktop/CHANGELOG.md b/crates/iron-remote-desktop/CHANGELOG.md new file mode 100644 index 00000000..2f076f11 --- /dev/null +++ b/crates/iron-remote-desktop/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.6.0...iron-remote-desktop-v0.7.0)] - 2025-09-29 + +### Bug Fixes + +- [**breaking**] Changed onClipboardChanged to not consume the input (#992) ([6127e13c83](https://github.com/Devolutions/IronRDP/commit/6127e13c836d06764d483b6b55188fd23a4314a2)) + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.5.0...iron-remote-desktop-v0.6.0)] - 2025-08-29 + +### Features + +- [**breaking**] Extend `DeviceEvent.wheelRotations` event to support passing rotation units other than pixels (#952) ([23c0cc2c36](https://github.com/Devolutions/IronRDP/commit/23c0cc2c365159d24330a89ec4015121b67bccb6)) + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.4.0...iron-remote-desktop-v0.5.0)] - 2025-08-29 + +### Bug Fixes + +- [**breaking**] Remove the `remote_received_format_list_callback` method from Session common API (#935) ([5b948e2161](https://github.com/Devolutions/IronRDP/commit/5b948e2161b08b13d32bdbb480b26c8fa44d42f7)) + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.3.0...iron-remote-desktop-v0.4.0)] - 2025-06-27 + +### Features + +- [**breaking**] Add `canvas_resized_callback` method to `SessionBuilder` trait (#842) ([f6285c5989](https://github.com/Devolutions/IronRDP/commit/f6285c598915c8afb07553c765648d85ac4140cb)) + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.2.0...iron-remote-desktop-v0.3.0)] - 2025-06-03 + +### Bug Fixes + +- [**breaking**] Rename extension_call to invoke_extension (#803) ([f68cd06ac3](https://github.com/Devolutions/IronRDP/commit/f68cd06ac3705608e6f2ac6bde684d9ae906ea53)) + + diff --git a/crates/iron-remote-desktop/Cargo.toml b/crates/iron-remote-desktop/Cargo.toml new file mode 100644 index 00000000..ba5d8a90 --- /dev/null +++ b/crates/iron-remote-desktop/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "iron-remote-desktop" +version = "0.7.0" +readme = "README.md" +description = "Helper crate for building WASM modules compatible with iron-remote-desktop WebComponent" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[features] +panic_hook = ["dep:console_error_panic_hook"] + +[dependencies] +# WASM +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["HtmlCanvasElement"] } +tracing-web = "0.1" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1", optional = true } + +# Logging +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["time"] } + +[lints] +workspace = true diff --git a/crates/iron-remote-desktop/README.md b/crates/iron-remote-desktop/README.md new file mode 100644 index 00000000..58ba88bf --- /dev/null +++ b/crates/iron-remote-desktop/README.md @@ -0,0 +1,8 @@ +# Iron Remote Desktop — Helper Crate + +Helper crate for building WASM modules compatible with the `iron-remote-desktop` WebComponent. + +Implement the `RemoteDesktopApi` trait on a Rust type, and call the `make_bridge!` on +it to generate the WASM API that is expected by `iron-remote-desktop`. + +See the `ironrdp-web` crate in the repository to see how it is used in practice. diff --git a/crates/iron-remote-desktop/src/clipboard.rs b/crates/iron-remote-desktop/src/clipboard.rs new file mode 100644 index 00000000..bd58b9d8 --- /dev/null +++ b/crates/iron-remote-desktop/src/clipboard.rs @@ -0,0 +1,23 @@ +use wasm_bindgen::JsValue; + +pub trait ClipboardData { + type Item: ClipboardItem; + + fn create() -> Self; + + fn add_text(&mut self, mime_type: &str, text: &str); + + fn add_binary(&mut self, mime_type: &str, binary: &[u8]); + + fn items(&self) -> &[Self::Item]; + + fn is_empty(&self) -> bool { + self.items().is_empty() + } +} + +pub trait ClipboardItem { + fn mime_type(&self) -> &str; + + fn value(&self) -> impl Into; +} diff --git a/crates/iron-remote-desktop/src/cursor.rs b/crates/iron-remote-desktop/src/cursor.rs new file mode 100644 index 00000000..1d760692 --- /dev/null +++ b/crates/iron-remote-desktop/src/cursor.rs @@ -0,0 +1,10 @@ +#[derive(Debug)] +pub enum CursorStyle { + Default, + Hidden, + Url { + data: String, + hotspot_x: u16, + hotspot_y: u16, + }, +} diff --git a/crates/iron-remote-desktop/src/desktop_size.rs b/crates/iron-remote-desktop/src/desktop_size.rs new file mode 100644 index 00000000..8d58226c --- /dev/null +++ b/crates/iron-remote-desktop/src/desktop_size.rs @@ -0,0 +1,16 @@ +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +#[derive(Clone, Copy)] +pub struct DesktopSize { + pub width: u16, + pub height: u16, +} + +#[wasm_bindgen] +impl DesktopSize { + #[wasm_bindgen(constructor)] + pub fn create(width: u16, height: u16) -> Self { + DesktopSize { width, height } + } +} diff --git a/crates/iron-remote-desktop/src/error.rs b/crates/iron-remote-desktop/src/error.rs new file mode 100644 index 00000000..42093844 --- /dev/null +++ b/crates/iron-remote-desktop/src/error.rs @@ -0,0 +1,26 @@ +use wasm_bindgen::prelude::*; + +pub trait IronError { + fn backtrace(&self) -> String; + + fn kind(&self) -> IronErrorKind; +} + +#[derive(Clone, Copy)] +#[wasm_bindgen] +pub enum IronErrorKind { + /// Catch-all error kind + General, + /// Incorrect password used + WrongPassword, + /// Unable to login to machine + LogonFailure, + /// Insufficient permission, server denied access + AccessDenied, + /// Something wrong happened when sending or receiving the RDCleanPath message + RDCleanPath, + /// Couldn’t connect to proxy + ProxyConnect, + /// Protocol negotiation failed + NegotiationFailure, +} diff --git a/crates/iron-remote-desktop/src/extension.rs b/crates/iron-remote-desktop/src/extension.rs new file mode 100644 index 00000000..519cb82a --- /dev/null +++ b/crates/iron-remote-desktop/src/extension.rs @@ -0,0 +1,76 @@ +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; + +#[macro_export] +macro_rules! extension_match { + ( @ $jsval:expr, $value:ident, String, $operation:block ) => {{ + if let Some($value) = $jsval.as_string() { + $operation + } else { + warn!("Unexpected value for extension {}", stringify!($ident)); + } + }}; + ( @ $jsval:expr, $value:ident, f64, $operation:block ) => {{ + if let Some($value) = $jsval.as_f64() { + $operation + } else { + warn!("Unexpected value for extension {}", stringify!($ident)); + } + }}; + ( @ $jsval:expr, $value:ident, bool, $operation:block ) => {{ + if let Some($value) = $jsval.as_bool() { + $operation + } else { + warn!("Unexpected value for extension {}", stringify!($ident)); + } + }}; + ( @ $jsval:expr, $value:ident, JsValue, $operation:block ) => {{ + let $value = $jsval; + $operation + }}; + + ( match $ext:ident ; $( | $value:ident : $ty:ident | $operation:block ; )* ) => { + let ident = $ext.ident(); + + match ident { + $( stringify!($value) => $crate::extension_match!( @ $ext.into_value(), $value, $ty, $operation ), )* + unknown_extension => ::tracing::warn!("Unknown extension: {unknown_extension}"), + } + }; +} + +#[wasm_bindgen] +pub struct Extension { + ident: String, + value: JsValue, +} + +#[wasm_bindgen] +impl Extension { + #[wasm_bindgen(constructor)] + pub fn create(ident: String, value: JsValue) -> Self { + Self { ident, value } + } +} + +#[expect( + clippy::allow_attributes, + reason = "Unfortunately, expect attribute doesn't work with clippy::multiple_inherent_impl lint" +)] +#[allow( + clippy::multiple_inherent_impl, + reason = "We don't want to expose these methods to JS" +)] +impl Extension { + pub fn ident(&self) -> &str { + self.ident.as_str() + } + + pub fn value(&self) -> &JsValue { + &self.value + } + + pub fn into_value(self) -> JsValue { + self.value + } +} diff --git a/crates/iron-remote-desktop/src/input.rs b/crates/iron-remote-desktop/src/input.rs new file mode 100644 index 00000000..a4b45135 --- /dev/null +++ b/crates/iron-remote-desktop/src/input.rs @@ -0,0 +1,34 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub enum RotationUnit { + Pixel, + Line, + Page, +} + +pub trait DeviceEvent { + fn mouse_button_pressed(button: u8) -> Self; + + fn mouse_button_released(button: u8) -> Self; + + fn mouse_move(x: u16, y: u16) -> Self; + + fn wheel_rotations(vertical: bool, rotation_amount: i16, rotation_unit: RotationUnit) -> Self; + + fn key_pressed(scancode: u16) -> Self; + + fn key_released(scancode: u16) -> Self; + + fn unicode_pressed(unicode: char) -> Self; + + fn unicode_released(unicode: char) -> Self; +} + +pub trait InputTransaction { + type DeviceEvent: DeviceEvent; + + fn create() -> Self; + + fn add_event(&mut self, event: Self::DeviceEvent); +} diff --git a/crates/iron-remote-desktop/src/lib.rs b/crates/iron-remote-desktop/src/lib.rs new file mode 100644 index 00000000..5dd8dd60 --- /dev/null +++ b/crates/iron-remote-desktop/src/lib.rs @@ -0,0 +1,480 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +mod clipboard; +mod cursor; +mod desktop_size; +mod error; +mod extension; +mod input; +mod session; + +pub use clipboard::{ClipboardData, ClipboardItem}; +pub use cursor::CursorStyle; +pub use desktop_size::DesktopSize; +pub use error::{IronError, IronErrorKind}; +pub use extension::Extension; +pub use input::{DeviceEvent, InputTransaction, RotationUnit}; +pub use session::{Session, SessionBuilder, SessionTerminationInfo}; + +pub trait RemoteDesktopApi { + type Session: Session; + type SessionBuilder: SessionBuilder; + type SessionTerminationInfo: SessionTerminationInfo; + type DeviceEvent: DeviceEvent; + type InputTransaction: InputTransaction; + type ClipboardData: ClipboardData; + type ClipboardItem: ClipboardItem; + type Error: IronError; + + /// Called before the logger is set. + fn pre_setup() {} + + /// Called after the logger is set. + fn post_setup() {} +} + +#[macro_export] +macro_rules! make_bridge { + ($api:ty) => { + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + pub struct Session(<$api as $crate::RemoteDesktopApi>::Session); + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + pub struct SessionBuilder(<$api as $crate::RemoteDesktopApi>::SessionBuilder); + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + pub struct SessionTerminationInfo(<$api as $crate::RemoteDesktopApi>::SessionTerminationInfo); + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + pub struct DeviceEvent(<$api as $crate::RemoteDesktopApi>::DeviceEvent); + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + pub struct InputTransaction(<$api as $crate::RemoteDesktopApi>::InputTransaction); + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + pub struct ClipboardData(<$api as $crate::RemoteDesktopApi>::ClipboardData); + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + pub struct ClipboardItem(<$api as $crate::RemoteDesktopApi>::ClipboardItem); + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + pub struct IronError(<$api as $crate::RemoteDesktopApi>::Error); + + impl From<<$api as $crate::RemoteDesktopApi>::Session> for Session { + fn from(value: <$api as $crate::RemoteDesktopApi>::Session) -> Self { + Self(value) + } + } + + impl From<<$api as $crate::RemoteDesktopApi>::SessionBuilder> for SessionBuilder { + fn from(value: <$api as $crate::RemoteDesktopApi>::SessionBuilder) -> Self { + Self(value) + } + } + + impl From<<$api as $crate::RemoteDesktopApi>::SessionTerminationInfo> for SessionTerminationInfo { + fn from(value: <$api as $crate::RemoteDesktopApi>::SessionTerminationInfo) -> Self { + Self(value) + } + } + + impl From<<$api as $crate::RemoteDesktopApi>::DeviceEvent> for DeviceEvent { + fn from(value: <$api as $crate::RemoteDesktopApi>::DeviceEvent) -> Self { + Self(value) + } + } + + impl From<<$api as $crate::RemoteDesktopApi>::InputTransaction> for InputTransaction { + fn from(value: <$api as $crate::RemoteDesktopApi>::InputTransaction) -> Self { + Self(value) + } + } + + impl From<<$api as $crate::RemoteDesktopApi>::ClipboardData> for ClipboardData { + fn from(value: <$api as $crate::RemoteDesktopApi>::ClipboardData) -> Self { + Self(value) + } + } + + impl From<<$api as $crate::RemoteDesktopApi>::ClipboardItem> for ClipboardItem { + fn from(value: <$api as $crate::RemoteDesktopApi>::ClipboardItem) -> Self { + Self(value) + } + } + + impl From<<$api as $crate::RemoteDesktopApi>::Error> for IronError { + fn from(value: <$api as $crate::RemoteDesktopApi>::Error) -> Self { + Self(value) + } + } + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + #[doc(hidden)] + pub fn setup(log_level: &str) { + <$api as $crate::RemoteDesktopApi>::pre_setup(); + $crate::internal::setup(log_level); + <$api as $crate::RemoteDesktopApi>::post_setup(); + } + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + #[doc(hidden)] + impl Session { + pub async fn run(&self) -> Result { + $crate::Session::run(&self.0) + .await + .map(SessionTerminationInfo) + .map_err(IronError) + } + + #[wasm_bindgen(js_name = desktopSize)] + pub fn desktop_size(&self) -> $crate::DesktopSize { + $crate::Session::desktop_size(&self.0) + } + + #[wasm_bindgen(js_name = applyInputs)] + pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), IronError> { + $crate::Session::apply_inputs(&self.0, transaction.0).map_err(IronError) + } + + #[wasm_bindgen(js_name = releaseAllInputs)] + pub fn release_all_inputs(&self) -> Result<(), IronError> { + $crate::Session::release_all_inputs(&self.0).map_err(IronError) + } + + #[wasm_bindgen(js_name = synchronizeLockKeys)] + pub fn synchronize_lock_keys( + &self, + scroll_lock: bool, + num_lock: bool, + caps_lock: bool, + kana_lock: bool, + ) -> Result<(), IronError> { + $crate::Session::synchronize_lock_keys(&self.0, scroll_lock, num_lock, caps_lock, kana_lock) + .map_err(IronError) + } + + pub fn shutdown(&self) -> Result<(), IronError> { + $crate::Session::shutdown(&self.0).map_err(IronError) + } + + #[wasm_bindgen(js_name = onClipboardPaste)] + pub async fn on_clipboard_paste(&self, content: &ClipboardData) -> Result<(), IronError> { + $crate::Session::on_clipboard_paste(&self.0, &content.0) + .await + .map_err(IronError) + } + + pub fn resize( + &self, + width: u32, + height: u32, + scale_factor: Option, + physical_width: Option, + physical_height: Option, + ) { + $crate::Session::resize( + &self.0, + width, + height, + scale_factor, + physical_width, + physical_height, + ); + } + + #[wasm_bindgen(js_name = supportsUnicodeKeyboardShortcuts)] + pub fn supports_unicode_keyboard_shortcuts(&self) -> bool { + $crate::Session::supports_unicode_keyboard_shortcuts(&self.0) + } + + #[wasm_bindgen(js_name = invokeExtension)] + pub fn invoke_extension( + &self, + ext: $crate::Extension, + ) -> Result<$crate::internal::wasm_bindgen::JsValue, IronError> { + <<$api as $crate::RemoteDesktopApi>::Session as $crate::Session>::invoke_extension(&self.0, ext) + .map_err(IronError) + } + } + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + #[doc(hidden)] + impl SessionBuilder { + #[wasm_bindgen(constructor)] + pub fn create() -> Self { + Self(<<$api as $crate::RemoteDesktopApi>::SessionBuilder as $crate::SessionBuilder>::create()) + } + + pub fn username(&self, username: String) -> Self { + Self($crate::SessionBuilder::username(&self.0, username)) + } + + pub fn destination(&self, destination: String) -> Self { + Self($crate::SessionBuilder::destination(&self.0, destination)) + } + + #[wasm_bindgen(js_name = serverDomain)] + pub fn server_domain(&self, server_domain: String) -> Self { + Self($crate::SessionBuilder::server_domain(&self.0, server_domain)) + } + + pub fn password(&self, password: String) -> Self { + Self($crate::SessionBuilder::password(&self.0, password)) + } + + #[wasm_bindgen(js_name = proxyAddress)] + pub fn proxy_address(&self, address: String) -> Self { + Self($crate::SessionBuilder::proxy_address(&self.0, address)) + } + + #[wasm_bindgen(js_name = authToken)] + pub fn auth_token(&self, token: String) -> Self { + Self($crate::SessionBuilder::auth_token(&self.0, token)) + } + + #[wasm_bindgen(js_name = desktopSize)] + pub fn desktop_size(&self, desktop_size: $crate::DesktopSize) -> Self { + Self($crate::SessionBuilder::desktop_size(&self.0, desktop_size)) + } + + #[wasm_bindgen(js_name = renderCanvas)] + pub fn render_canvas(&self, canvas: $crate::internal::web_sys::HtmlCanvasElement) -> Self { + Self($crate::SessionBuilder::render_canvas(&self.0, canvas)) + } + + #[wasm_bindgen(js_name = setCursorStyleCallback)] + pub fn set_cursor_style_callback(&self, callback: $crate::internal::web_sys::js_sys::Function) -> Self { + Self($crate::SessionBuilder::set_cursor_style_callback( + &self.0, callback, + )) + } + + #[wasm_bindgen(js_name = setCursorStyleCallbackContext)] + pub fn set_cursor_style_callback_context(&self, context: $crate::internal::wasm_bindgen::JsValue) -> Self { + Self($crate::SessionBuilder::set_cursor_style_callback_context( + &self.0, context, + )) + } + + #[wasm_bindgen(js_name = remoteClipboardChangedCallback)] + pub fn remote_clipboard_changed_callback( + &self, + callback: $crate::internal::web_sys::js_sys::Function, + ) -> Self { + Self($crate::SessionBuilder::remote_clipboard_changed_callback( + &self.0, callback, + )) + } + + #[wasm_bindgen(js_name = forceClipboardUpdateCallback)] + pub fn force_clipboard_update_callback( + &self, + callback: $crate::internal::web_sys::js_sys::Function, + ) -> Self { + Self($crate::SessionBuilder::force_clipboard_update_callback( + &self.0, callback, + )) + } + + #[wasm_bindgen(js_name = canvasResizedCallback)] + pub fn canvas_resized_callback(&self, callback: $crate::internal::web_sys::js_sys::Function) -> Self { + Self($crate::SessionBuilder::canvas_resized_callback(&self.0, callback)) + } + + pub fn extension(&self, ext: $crate::Extension) -> Self { + Self($crate::SessionBuilder::extension(&self.0, ext)) + } + + pub async fn connect(&self) -> Result { + $crate::SessionBuilder::connect(&self.0) + .await + .map(Session) + .map_err(IronError) + } + } + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + #[doc(hidden)] + impl SessionTerminationInfo { + pub fn reason(&self) -> String { + $crate::SessionTerminationInfo::reason(&self.0) + } + } + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + #[doc(hidden)] + impl DeviceEvent { + #[wasm_bindgen(js_name = mouseButtonPressed)] + pub fn mouse_button_pressed(button: u8) -> Self { + Self( + <<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::mouse_button_pressed( + button, + ), + ) + } + + #[wasm_bindgen(js_name = mouseButtonReleased)] + pub fn mouse_button_released(button: u8) -> Self { + Self( + <<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::mouse_button_released( + button, + ), + ) + } + + #[wasm_bindgen(js_name = mouseMove)] + pub fn mouse_move(x: u16, y: u16) -> Self { + Self(<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::mouse_move(x, y)) + } + + #[wasm_bindgen(js_name = wheelRotations)] + pub fn wheel_rotations(vertical: bool, rotation_amount: i16, rotation_unit: $crate::RotationUnit) -> Self { + Self( + <<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::wheel_rotations( + vertical, + rotation_amount, + rotation_unit, + ), + ) + } + + #[wasm_bindgen(js_name = keyPressed)] + pub fn key_pressed(scancode: u16) -> Self { + Self(<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::key_pressed(scancode)) + } + + #[wasm_bindgen(js_name = keyReleased)] + pub fn key_released(scancode: u16) -> Self { + Self(<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::key_released(scancode)) + } + + #[wasm_bindgen(js_name = unicodePressed)] + pub fn unicode_pressed(unicode: char) -> Self { + Self(<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::unicode_pressed(unicode)) + } + + #[wasm_bindgen(js_name = unicodeReleased)] + pub fn unicode_released(unicode: char) -> Self { + Self( + <<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::unicode_released(unicode), + ) + } + } + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + #[doc(hidden)] + impl InputTransaction { + #[wasm_bindgen(constructor)] + pub fn create() -> Self { + Self(<<$api as $crate::RemoteDesktopApi>::InputTransaction as $crate::InputTransaction>::create()) + } + + #[wasm_bindgen(js_name = addEvent)] + pub fn add_event(&mut self, event: DeviceEvent) { + $crate::InputTransaction::add_event(&mut self.0, event.0); + } + } + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + #[doc(hidden)] + impl ClipboardData { + #[wasm_bindgen(constructor)] + pub fn create() -> Self { + Self(<<$api as $crate::RemoteDesktopApi>::ClipboardData as $crate::ClipboardData>::create()) + } + + #[wasm_bindgen(js_name = addText)] + pub fn add_text(&mut self, mime_type: &str, text: &str) { + $crate::ClipboardData::add_text(&mut self.0, mime_type, text); + } + + #[wasm_bindgen(js_name = addBinary)] + pub fn add_binary(&mut self, mime_type: &str, binary: &[u8]) { + $crate::ClipboardData::add_binary(&mut self.0, mime_type, binary); + } + + pub fn items(&self) -> Vec { + $crate::ClipboardData::items(&self.0) + .into_iter() + .cloned() + .map(ClipboardItem) + .collect() + } + + #[wasm_bindgen(js_name = isEmpty)] + pub fn is_empty(&self) -> bool { + $crate::ClipboardData::is_empty(&self.0) + } + } + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + #[doc(hidden)] + impl ClipboardItem { + #[wasm_bindgen(js_name = mimeType)] + pub fn mime_type(&self) -> String { + $crate::ClipboardItem::mime_type(&self.0).to_owned() + } + + pub fn value(&self) -> $crate::internal::wasm_bindgen::JsValue { + $crate::ClipboardItem::value(&self.0).into() + } + } + + #[$crate::internal::wasm_bindgen::prelude::wasm_bindgen] + #[doc(hidden)] + impl IronError { + pub fn backtrace(&self) -> String { + $crate::IronError::backtrace(&self.0) + } + + pub fn kind(&self) -> $crate::IronErrorKind { + $crate::IronError::kind(&self.0) + } + } + }; +} + +#[doc(hidden)] +pub mod internal { + #[doc(hidden)] + pub use wasm_bindgen; + #[doc(hidden)] + pub use web_sys; + + #[doc(hidden)] + pub fn setup(log_level: &str) { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "panic_hook")] + console_error_panic_hook::set_once(); + + if let Ok(level) = log_level.parse::() { + set_logger_once(level); + } + } + + fn set_logger_once(level: tracing::Level) { + use tracing_subscriber::filter::LevelFilter; + use tracing_subscriber::fmt::time::UtcTime; + use tracing_subscriber::prelude::*; + use tracing_web::MakeConsoleWriter; + + static INIT: std::sync::Once = std::sync::Once::new(); + + INIT.call_once(|| { + let fmt_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers + .with_writer(MakeConsoleWriter); + + let level_filter = LevelFilter::from_level(level); + + tracing_subscriber::registry().with(fmt_layer).with(level_filter).init(); + }) + } +} diff --git a/crates/iron-remote-desktop/src/session.rs b/crates/iron-remote-desktop/src/session.rs new file mode 100644 index 00000000..d9f973d8 --- /dev/null +++ b/crates/iron-remote-desktop/src/session.rs @@ -0,0 +1,106 @@ +use wasm_bindgen::JsValue; +use web_sys::{js_sys, HtmlCanvasElement}; + +use crate::clipboard::ClipboardData; +use crate::error::IronError; +use crate::input::InputTransaction; +use crate::{DesktopSize, Extension}; + +pub trait SessionBuilder { + type Session: Session; + type Error: IronError; + + fn create() -> Self; + + #[must_use] + fn username(&self, username: String) -> Self; + + #[must_use] + fn destination(&self, destination: String) -> Self; + + #[must_use] + fn server_domain(&self, server_domain: String) -> Self; + + #[must_use] + fn password(&self, password: String) -> Self; + + #[must_use] + fn proxy_address(&self, address: String) -> Self; + + #[must_use] + fn auth_token(&self, token: String) -> Self; + + #[must_use] + fn desktop_size(&self, desktop_size: DesktopSize) -> Self; + + #[must_use] + fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self; + + #[must_use] + fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self; + + #[must_use] + fn set_cursor_style_callback_context(&self, context: JsValue) -> Self; + + #[must_use] + fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> Self; + + #[must_use] + fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self; + + #[must_use] + fn canvas_resized_callback(&self, callback: js_sys::Function) -> Self; + + #[must_use] + fn extension(&self, ext: Extension) -> Self; + + #[expect(async_fn_in_trait)] + async fn connect(&self) -> Result; +} + +pub trait Session { + type SessionTerminationInfo: SessionTerminationInfo; + type InputTransaction: InputTransaction; + type ClipboardData: ClipboardData; + type Error: IronError; + + fn run(&self) -> impl core::future::Future>; + + fn desktop_size(&self) -> DesktopSize; + + fn apply_inputs(&self, transaction: Self::InputTransaction) -> Result<(), Self::Error>; + + fn release_all_inputs(&self) -> Result<(), Self::Error>; + + fn synchronize_lock_keys( + &self, + scroll_lock: bool, + num_lock: bool, + caps_lock: bool, + kana_lock: bool, + ) -> Result<(), Self::Error>; + + fn shutdown(&self) -> Result<(), Self::Error>; + + fn on_clipboard_paste( + &self, + content: &Self::ClipboardData, + ) -> impl core::future::Future>; + + fn resize( + &self, + width: u32, + height: u32, + scale_factor: Option, + physical_width: Option, + physical_height: Option, + ); + + fn supports_unicode_keyboard_shortcuts(&self) -> bool; + + fn invoke_extension(&self, ext: Extension) -> Result; +} + +pub trait SessionTerminationInfo { + fn reason(&self) -> String; +} diff --git a/crates/ironrdp-acceptor/CHANGELOG.md b/crates/ironrdp-acceptor/CHANGELOG.md new file mode 100644 index 00000000..4918e46f --- /dev/null +++ b/crates/ironrdp-acceptor/CHANGELOG.md @@ -0,0 +1,77 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.7.0...ironrdp-acceptor-v0.8.0)] - 2025-12-18 + +### Bug Fixes + +- [**breaking**] Use static dispatch for NetworkClient trait ([#1043](https://github.com/Devolutions/IronRDP/issues/1043)) ([bca6d190a8](https://github.com/Devolutions/IronRDP/commit/bca6d190a870708468534d224ff225a658767a9a)) + + - Rename `AsyncNetworkClient` to `NetworkClient` + - Replace dynamic dispatch (`Option<&mut dyn ...>`) with static dispatch + using generics (`&mut N where N: NetworkClient`) + - Reorder `connect_finalize` parameters for consistency across crates + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.5.0...ironrdp-acceptor-v0.6.0)] - 2025-07-08 + +### Features + +- [**breaking**] Support for server-side Kerberos (#839) ([33530212c4](https://github.com/Devolutions/IronRDP/commit/33530212c42bf28c875ac078ed2408657831b417)) + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.4.0...ironrdp-acceptor-v0.5.0)] - 2025-05-27 + +### Features + +- Make the CredsspSequence type public ([5abd9ff8e0](https://github.com/Devolutions/IronRDP/commit/5abd9ff8e0da8ea48c6747526c4b703a39bf4972)) + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.3.1...ironrdp-acceptor-v0.4.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.3.0...ironrdp-acceptor-v0.3.1)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.2.1...ironrdp-acceptor-v0.3.0)] - 2025-01-28 + +### Security + +- Allow using basic RDP/no security ([7c72a9f9bb](https://github.com/Devolutions/IronRDP/commit/7c72a9f9bbe726d6f9f2377c19e9a672d8d086d5)) + +### Bug Fixes + +- Drop unexpected PDUs during deactivation-reactivation ([63963182b5](https://github.com/Devolutions/IronRDP/commit/63963182b5af6ad45dc638e93de4b8a0b565c7d3)) + + The current behavior of handling unmatched PDUs in fn read_by_hint() + isn't good enough. An unexpected PDUs may be received and fail to be + decoded during Acceptor::step(). + + Change the code to simply drop unexpected PDUs (as opposed to attempting + to replay the unmatched leftover, which isn't clearly needed) + +- Reattach existing channels ([c4587b537c](https://github.com/Devolutions/IronRDP/commit/c4587b537c7c0a148e11bc365bc3df88e2c92312)) + + I couldn't find any explicit behaviour described in the specification, + but apparently, we must just keep the channel state as they were during + reactivation. This fixes various state issues during client resize. + +- Do not restart static channels on reactivation ([82c7c2f5b0](https://github.com/Devolutions/IronRDP/commit/82c7c2f5b08c44b1a4f6b04c13ad24d9e2ffa371)) + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.2.0...ironrdp-acceptor-v0.2.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-acceptor/Cargo.toml b/crates/ironrdp-acceptor/Cargo.toml new file mode 100644 index 00000000..a3c42b9d --- /dev/null +++ b/crates/ironrdp-acceptor/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ironrdp-acceptor" +version = "0.8.0" +readme = "README.md" +description = "State machines to drive an RDP connection acceptance sequence" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.8" } # public +ironrdp-async = { path = "../ironrdp-async", version = "0.8" } # public +tracing = { version = "0.1", features = ["log"] } + +[lints] +workspace = true + diff --git a/crates/ironrdp-acceptor/LICENSE-APACHE b/crates/ironrdp-acceptor/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-acceptor/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-acceptor/LICENSE-MIT b/crates/ironrdp-acceptor/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-acceptor/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-acceptor/README.md b/crates/ironrdp-acceptor/README.md new file mode 100644 index 00000000..63695d65 --- /dev/null +++ b/crates/ironrdp-acceptor/README.md @@ -0,0 +1,9 @@ +# IronRDP Acceptor + +State machines to drive an RDP connection acceptance sequence. + +For now, it requires the [Tokio runtime](https://tokio.rs/). + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-acceptor/src/channel_connection.rs b/crates/ironrdp-acceptor/src/channel_connection.rs new file mode 100644 index 00000000..62bd6cd3 --- /dev/null +++ b/crates/ironrdp-acceptor/src/channel_connection.rs @@ -0,0 +1,198 @@ +use std::collections::HashSet; + +use ironrdp_connector::{ + reason_err, ConnectorError, ConnectorErrorExt as _, ConnectorResult, Sequence, State, Written, +}; +use ironrdp_core::WriteBuf; +use ironrdp_pdu::mcs; +use ironrdp_pdu::x224::X224; +use tracing::debug; + +#[derive(Debug)] +pub struct ChannelConnectionSequence { + state: ChannelConnectionState, + user_channel_id: u16, + channel_ids: Option>, +} + +#[derive(Default, Debug)] +pub enum ChannelConnectionState { + #[default] + Consumed, + + WaitErectDomainRequest, + WaitAttachUserRequest, + SendAttachUserConfirm, + WaitChannelJoinRequest { + remaining: HashSet, + }, + SendChannelJoinConfirm { + remaining: HashSet, + channel_id: u16, + }, + AllJoined, +} + +impl State for ChannelConnectionState { + fn name(&self) -> &'static str { + match self { + Self::Consumed => "Consumed", + Self::WaitErectDomainRequest => "WaitErectDomainRequest", + Self::WaitAttachUserRequest => "WaitAttachUserRequest", + Self::SendAttachUserConfirm => "SendAttachUserConfirm", + Self::WaitChannelJoinRequest { .. } => "WaitChannelJoinRequest", + Self::SendChannelJoinConfirm { .. } => "SendChannelJoinConfirm", + Self::AllJoined { .. } => "AllJoined", + } + } + + fn is_terminal(&self) -> bool { + matches!(self, Self::AllJoined { .. }) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +impl Sequence for ChannelConnectionSequence { + fn next_pdu_hint(&self) -> Option<&dyn ironrdp_pdu::PduHint> { + match &self.state { + ChannelConnectionState::Consumed => None, + ChannelConnectionState::WaitErectDomainRequest => Some(&ironrdp_pdu::X224_HINT), + ChannelConnectionState::WaitAttachUserRequest => Some(&ironrdp_pdu::X224_HINT), + ChannelConnectionState::SendAttachUserConfirm => None, + ChannelConnectionState::WaitChannelJoinRequest { .. } => Some(&ironrdp_pdu::X224_HINT), + ChannelConnectionState::SendChannelJoinConfirm { .. } => None, + ChannelConnectionState::AllJoined => None, + } + } + + fn state(&self) -> &dyn State { + &self.state + } + + fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult { + let (written, next_state) = match core::mem::take(&mut self.state) { + ChannelConnectionState::WaitErectDomainRequest => { + let erect_domain_request = ironrdp_core::decode::>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + + debug!(message = ?erect_domain_request, "Received"); + + (Written::Nothing, ChannelConnectionState::WaitAttachUserRequest) + } + + ChannelConnectionState::WaitAttachUserRequest => { + let attach_user_request = ironrdp_core::decode::>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + + debug!(message = ?attach_user_request, "Received"); + + (Written::Nothing, ChannelConnectionState::SendAttachUserConfirm) + } + + ChannelConnectionState::SendAttachUserConfirm => { + let attach_user_confirm = mcs::AttachUserConfirm { + result: 0, + initiator_id: self.user_channel_id, + }; + + debug!(message = ?attach_user_confirm, "Send"); + + let written = + ironrdp_core::encode_buf(&X224(attach_user_confirm), output).map_err(ConnectorError::encode)?; + + let next_state = match self.channel_ids.take() { + Some(channel_ids) => ChannelConnectionState::WaitChannelJoinRequest { remaining: channel_ids }, + None => ChannelConnectionState::AllJoined, + }; + + (Written::from_size(written)?, next_state) + } + + ChannelConnectionState::WaitChannelJoinRequest { mut remaining } => { + let channel_request = ironrdp_core::decode::>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + + debug!(message = ?channel_request, "Received"); + + let is_expected = remaining.remove(&channel_request.channel_id); + + if !is_expected { + return Err(reason_err!( + "ChannelJoinConfirm", + "unexpected channel_id in MCS Channel Join Request: got {}, expected one of: {:?}", + channel_request.channel_id, + remaining, + )); + } + + ( + Written::Nothing, + ChannelConnectionState::SendChannelJoinConfirm { + remaining, + channel_id: channel_request.channel_id, + }, + ) + } + + ChannelConnectionState::SendChannelJoinConfirm { remaining, channel_id } => { + let channel_confirm = mcs::ChannelJoinConfirm { + result: 0, + initiator_id: self.user_channel_id, + requested_channel_id: channel_id, + channel_id, + }; + + debug!(message = ?channel_confirm, "Send"); + + let written = + ironrdp_core::encode_buf(&X224(channel_confirm), output).map_err(ConnectorError::encode)?; + + let next_state = if remaining.is_empty() { + ChannelConnectionState::AllJoined + } else { + ChannelConnectionState::WaitChannelJoinRequest { remaining } + }; + + (Written::from_size(written)?, next_state) + } + + _ => unreachable!(), + }; + + self.state = next_state; + Ok(written) + } +} + +impl ChannelConnectionSequence { + pub fn new(user_channel_id: u16, io_channel_id: u16, other_channels: Vec) -> Self { + Self { + state: ChannelConnectionState::WaitErectDomainRequest, + user_channel_id, + channel_ids: Some( + vec![user_channel_id, io_channel_id] + .into_iter() + .chain(other_channels) + .collect(), + ), + } + } + + pub fn skip_channel_join(user_channel_id: u16) -> Self { + Self { + state: ChannelConnectionState::WaitErectDomainRequest, + user_channel_id, + channel_ids: None, + } + } + + pub fn is_done(&self) -> bool { + self.state.is_terminal() + } +} diff --git a/crates/ironrdp-acceptor/src/connection.rs b/crates/ironrdp-acceptor/src/connection.rs new file mode 100644 index 00000000..6643b941 --- /dev/null +++ b/crates/ironrdp-acceptor/src/connection.rs @@ -0,0 +1,760 @@ +use core::mem; + +use ironrdp_connector::{ + encode_x224_packet, general_err, reason_err, ConnectorError, ConnectorErrorExt as _, ConnectorResult, DesktopSize, + Sequence, State, Written, +}; +use ironrdp_core::{decode, WriteBuf}; +use ironrdp_pdu as pdu; +use ironrdp_pdu::nego::SecurityProtocol; +use ironrdp_pdu::x224::X224; +use ironrdp_svc::{StaticChannelSet, SvcServerProcessor}; +use pdu::rdp::capability_sets::CapabilitySet; +use pdu::rdp::client_info::Credentials; +use pdu::rdp::headers::ShareControlPdu; +use pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu}; +use pdu::rdp::server_license::{LicensePdu, LicensingErrorMessage}; +use pdu::{gcc, mcs, nego, rdp}; +use tracing::{debug, warn}; + +use super::channel_connection::ChannelConnectionSequence; +use super::finalization::FinalizationSequence; +use crate::util::{self, wrap_share_data}; + +const IO_CHANNEL_ID: u16 = 1003; +const USER_CHANNEL_ID: u16 = 1002; + +pub struct Acceptor { + pub(crate) state: AcceptorState, + security: SecurityProtocol, + io_channel_id: u16, + user_channel_id: u16, + desktop_size: DesktopSize, + server_capabilities: Vec, + static_channels: StaticChannelSet, + saved_for_reactivation: AcceptorState, + pub(crate) creds: Option, + reactivation: bool, +} + +#[derive(Debug)] +pub struct AcceptorResult { + pub static_channels: StaticChannelSet, + pub capabilities: Vec, + pub input_events: Vec>, + pub user_channel_id: u16, + pub io_channel_id: u16, + pub reactivation: bool, +} + +impl Acceptor { + pub fn new( + security: SecurityProtocol, + desktop_size: DesktopSize, + capabilities: Vec, + creds: Option, + ) -> Self { + Self { + security, + state: AcceptorState::InitiationWaitRequest, + user_channel_id: USER_CHANNEL_ID, + io_channel_id: IO_CHANNEL_ID, + desktop_size, + server_capabilities: capabilities, + static_channels: StaticChannelSet::new(), + saved_for_reactivation: Default::default(), + creds, + reactivation: false, + } + } + + pub fn new_deactivation_reactivation( + mut consumed: Acceptor, + static_channels: StaticChannelSet, + desktop_size: DesktopSize, + ) -> ConnectorResult { + let AcceptorState::CapabilitiesSendServer { + early_capability, + channels, + } = consumed.saved_for_reactivation + else { + return Err(general_err!("invalid acceptor state")); + }; + + for cap in consumed.server_capabilities.iter_mut() { + if let CapabilitySet::Bitmap(cap) = cap { + cap.desktop_width = desktop_size.width; + cap.desktop_height = desktop_size.height; + } + } + let state = AcceptorState::CapabilitiesSendServer { + early_capability, + channels: channels.clone(), + }; + let saved_for_reactivation = AcceptorState::CapabilitiesSendServer { + early_capability, + channels, + }; + Ok(Self { + security: consumed.security, + state, + user_channel_id: consumed.user_channel_id, + io_channel_id: consumed.io_channel_id, + desktop_size, + server_capabilities: consumed.server_capabilities, + static_channels, + saved_for_reactivation, + creds: consumed.creds, + reactivation: true, + }) + } + + pub fn attach_static_channel(&mut self, channel: T) + where + T: SvcServerProcessor + 'static, + { + self.static_channels.insert(channel); + } + + pub fn reached_security_upgrade(&self) -> Option { + match self.state { + AcceptorState::SecurityUpgrade { .. } => Some(self.security), + _ => None, + } + } + + /// # Panics + /// + /// Panics if state is not [AcceptorState::SecurityUpgrade]. + pub fn mark_security_upgrade_as_done(&mut self) { + assert!(self.reached_security_upgrade().is_some()); + self.step(&[], &mut WriteBuf::new()).expect("transition to next state"); + debug_assert!(self.reached_security_upgrade().is_none()); + } + + pub fn should_perform_credssp(&self) -> bool { + matches!(self.state, AcceptorState::Credssp { .. }) + } + + /// # Panics + /// + /// Panics if state is not [AcceptorState::Credssp]. + pub fn mark_credssp_as_done(&mut self) { + assert!(self.should_perform_credssp()); + let res = self.step(&[], &mut WriteBuf::new()).expect("transition to next state"); + debug_assert!(!self.should_perform_credssp()); + assert_eq!(res, Written::Nothing); + } + + pub fn get_result(&mut self) -> Option { + match mem::take(&mut self.state) { + AcceptorState::Accepted { + channels: _channels, // TODO: what about ChannelDef? + client_capabilities, + input_events, + } => Some(AcceptorResult { + static_channels: mem::take(&mut self.static_channels), + capabilities: client_capabilities, + input_events, + user_channel_id: self.user_channel_id, + io_channel_id: self.io_channel_id, + reactivation: self.reactivation, + }), + previous_state => { + self.state = previous_state; + None + } + } + } +} + +#[derive(Default, Debug)] +pub enum AcceptorState { + #[default] + Consumed, + + InitiationWaitRequest, + InitiationSendConfirm { + requested_protocol: SecurityProtocol, + }, + SecurityUpgrade { + requested_protocol: SecurityProtocol, + protocol: SecurityProtocol, + }, + Credssp { + requested_protocol: SecurityProtocol, + protocol: SecurityProtocol, + }, + BasicSettingsWaitInitial { + requested_protocol: SecurityProtocol, + protocol: SecurityProtocol, + }, + BasicSettingsSendResponse { + requested_protocol: SecurityProtocol, + protocol: SecurityProtocol, + early_capability: Option, + channels: Vec<(u16, Option)>, + }, + ChannelConnection { + protocol: SecurityProtocol, + early_capability: Option, + channels: Vec<(u16, gcc::ChannelDef)>, + connection: ChannelConnectionSequence, + }, + RdpSecurityCommencement { + protocol: SecurityProtocol, + early_capability: Option, + channels: Vec<(u16, gcc::ChannelDef)>, + }, + SecureSettingsExchange { + protocol: SecurityProtocol, + early_capability: Option, + channels: Vec<(u16, gcc::ChannelDef)>, + }, + LicensingExchange { + early_capability: Option, + channels: Vec<(u16, gcc::ChannelDef)>, + }, + CapabilitiesSendServer { + early_capability: Option, + channels: Vec<(u16, gcc::ChannelDef)>, + }, + MonitorLayoutSend { + channels: Vec<(u16, gcc::ChannelDef)>, + }, + CapabilitiesWaitConfirm { + channels: Vec<(u16, gcc::ChannelDef)>, + }, + ConnectionFinalization { + finalization: FinalizationSequence, + channels: Vec<(u16, gcc::ChannelDef)>, + client_capabilities: Vec, + }, + Accepted { + channels: Vec<(u16, gcc::ChannelDef)>, + client_capabilities: Vec, + input_events: Vec>, + }, +} + +impl State for AcceptorState { + fn name(&self) -> &'static str { + match self { + Self::Consumed => "Consumed", + Self::InitiationWaitRequest => "InitiationWaitRequest", + Self::InitiationSendConfirm { .. } => "InitiationSendConfirm", + Self::SecurityUpgrade { .. } => "SecurityUpgrade", + Self::Credssp { .. } => "Credssp", + Self::BasicSettingsWaitInitial { .. } => "BasicSettingsWaitInitial", + Self::BasicSettingsSendResponse { .. } => "BasicSettingsSendResponse", + Self::ChannelConnection { .. } => "ChannelConnection", + Self::RdpSecurityCommencement { .. } => "RdpSecurityCommencement", + Self::SecureSettingsExchange { .. } => "SecureSettingsExchange", + Self::LicensingExchange { .. } => "LicensingExchange", + Self::CapabilitiesSendServer { .. } => "CapabilitiesSendServer", + Self::MonitorLayoutSend { .. } => "MonitorLayoutSend", + Self::CapabilitiesWaitConfirm { .. } => "CapabilitiesWaitConfirm", + Self::ConnectionFinalization { .. } => "ConnectionFinalization", + Self::Accepted { .. } => "Connected", + } + } + + fn is_terminal(&self) -> bool { + matches!(self, Self::Accepted { .. }) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +impl Sequence for Acceptor { + fn next_pdu_hint(&self) -> Option<&dyn pdu::PduHint> { + match &self.state { + AcceptorState::Consumed => None, + AcceptorState::InitiationWaitRequest => Some(&pdu::X224_HINT), + AcceptorState::InitiationSendConfirm { .. } => None, + AcceptorState::SecurityUpgrade { .. } => None, + AcceptorState::Credssp { .. } => None, + AcceptorState::BasicSettingsWaitInitial { .. } => Some(&pdu::X224_HINT), + AcceptorState::BasicSettingsSendResponse { .. } => None, + AcceptorState::ChannelConnection { connection, .. } => connection.next_pdu_hint(), + AcceptorState::RdpSecurityCommencement { .. } => None, + AcceptorState::SecureSettingsExchange { .. } => Some(&pdu::X224_HINT), + AcceptorState::LicensingExchange { .. } => None, + AcceptorState::CapabilitiesSendServer { .. } => None, + AcceptorState::MonitorLayoutSend { .. } => None, + AcceptorState::CapabilitiesWaitConfirm { .. } => Some(&pdu::X224_HINT), + AcceptorState::ConnectionFinalization { finalization, .. } => finalization.next_pdu_hint(), + AcceptorState::Accepted { .. } => None, + } + } + + fn state(&self) -> &dyn State { + &self.state + } + + fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult { + let prev_state = mem::take(&mut self.state); + + let (written, next_state) = match prev_state { + AcceptorState::InitiationWaitRequest => { + let connection_request = decode::>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + + debug!(message = ?connection_request, "Received"); + + ( + Written::Nothing, + AcceptorState::InitiationSendConfirm { + requested_protocol: connection_request.protocol, + }, + ) + } + + AcceptorState::InitiationSendConfirm { requested_protocol } => { + let protocols = requested_protocol & self.security; + let protocol = if protocols.intersects(SecurityProtocol::HYBRID_EX) { + SecurityProtocol::HYBRID_EX + } else if protocols.intersects(SecurityProtocol::HYBRID) { + SecurityProtocol::HYBRID + } else if protocols.intersects(SecurityProtocol::SSL) { + SecurityProtocol::SSL + } else if self.security.is_empty() { + SecurityProtocol::empty() + } else { + return Err(ConnectorError::general("failed to negotiate security protocol")); + }; + let connection_confirm = nego::ConnectionConfirm::Response { + flags: nego::ResponseFlags::empty(), + protocol, + }; + + debug!(message = ?connection_confirm, "Send"); + + let written = + ironrdp_core::encode_buf(&X224(connection_confirm), output).map_err(ConnectorError::encode)?; + + ( + Written::from_size(written)?, + AcceptorState::SecurityUpgrade { + requested_protocol, + protocol, + }, + ) + } + + AcceptorState::SecurityUpgrade { + requested_protocol, + protocol, + } => { + debug!(?requested_protocol); + let next_state = if protocol.intersects(SecurityProtocol::HYBRID | SecurityProtocol::HYBRID_EX) { + AcceptorState::Credssp { + requested_protocol, + protocol, + } + } else { + AcceptorState::BasicSettingsWaitInitial { + requested_protocol, + protocol, + } + }; + (Written::Nothing, next_state) + } + + AcceptorState::Credssp { + requested_protocol, + protocol, + } => ( + Written::Nothing, + AcceptorState::BasicSettingsWaitInitial { + requested_protocol, + protocol, + }, + ), + + AcceptorState::BasicSettingsWaitInitial { + requested_protocol, + protocol, + } => { + let x224_payload = decode::>>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + let settings_initial = + decode::(x224_payload.data.as_ref()).map_err(ConnectorError::decode)?; + + debug!(message = ?settings_initial, "Received"); + + let gcc_blocks = settings_initial.conference_create_request.into_gcc_blocks(); + let early_capability = gcc_blocks.core.optional_data.early_capability_flags; + + let joined: Vec<_> = gcc_blocks + .network + .map(|network| { + network + .channels + .into_iter() + .map(|c| { + self.static_channels + .get_by_channel_name(&c.name) + .map(|(type_id, _)| (type_id, c)) + }) + .collect() + }) + .unwrap_or_default(); + + #[expect(clippy::arithmetic_side_effects)] // IO channel ID is not big enough for overflowing. + let channels = joined + .into_iter() + .enumerate() + .map(|(i, channel)| { + let channel_id = u16::try_from(i).expect("always in the range") + self.io_channel_id + 1; + if let Some((type_id, c)) = channel { + self.static_channels.attach_channel_id(type_id, channel_id); + (channel_id, Some(c)) + } else { + (channel_id, None) + } + }) + .collect(); + + ( + Written::Nothing, + AcceptorState::BasicSettingsSendResponse { + requested_protocol, + protocol, + early_capability, + channels, + }, + ) + } + + AcceptorState::BasicSettingsSendResponse { + requested_protocol, + protocol, + early_capability, + channels, + } => { + let channel_ids: Vec = channels.iter().map(|&(i, _)| i).collect(); + + let skip_channel_join = early_capability + .is_some_and(|client| client.contains(gcc::ClientEarlyCapabilityFlags::SUPPORT_SKIP_CHANNELJOIN)); + + let server_blocks = create_gcc_blocks( + self.io_channel_id, + channel_ids.clone(), + requested_protocol, + skip_channel_join, + ); + + let settings_response = mcs::ConnectResponse { + conference_create_response: gcc::ConferenceCreateResponse::new(self.user_channel_id, server_blocks) + .map_err(ConnectorError::decode)?, + called_connect_id: 1, + domain_parameters: mcs::DomainParameters::target(), + }; + + debug!(message = ?settings_response, "Send"); + + let written = encode_x224_packet(&settings_response, output)?; + let channels = channels.into_iter().filter_map(|(i, c)| c.map(|c| (i, c))).collect(); + + ( + Written::from_size(written)?, + AcceptorState::ChannelConnection { + protocol, + early_capability, + channels, + connection: if skip_channel_join { + ChannelConnectionSequence::skip_channel_join(self.user_channel_id) + } else { + ChannelConnectionSequence::new(self.user_channel_id, self.io_channel_id, channel_ids) + }, + }, + ) + } + + AcceptorState::ChannelConnection { + protocol, + early_capability, + channels, + mut connection, + } => { + let written = connection.step(input, output)?; + let state = if connection.is_done() { + AcceptorState::RdpSecurityCommencement { + protocol, + early_capability, + channels, + } + } else { + AcceptorState::ChannelConnection { + protocol, + early_capability, + channels, + connection, + } + }; + + (written, state) + } + + AcceptorState::RdpSecurityCommencement { + protocol, + early_capability, + channels, + .. + } => ( + Written::Nothing, + AcceptorState::SecureSettingsExchange { + protocol, + early_capability, + channels, + }, + ), + + AcceptorState::SecureSettingsExchange { + protocol, + early_capability, + channels, + } => { + let data: X224> = decode(input).map_err(ConnectorError::decode)?; + let data = data.0; + let client_info: rdp::ClientInfoPdu = + decode(data.user_data.as_ref()).map_err(ConnectorError::decode)?; + + debug!(message = ?client_info, "Received"); + + if !protocol.intersects(SecurityProtocol::HYBRID | SecurityProtocol::HYBRID_EX) { + let creds = client_info.client_info.credentials; + + if self.creds.as_ref() != Some(&creds) { + // FIXME: How authorization should be denied with standard RDP security? + // Since standard RDP security is not a priority, we just send a ServerDeniedConnection ServerSetErrorInfo PDU. + let info = ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode( + ProtocolIndependentCode::ServerDeniedConnection, + )); + + debug!(message = ?info, "Send"); + + util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &info, output)?; + + return Err(ConnectorError::general("invalid credentials")); + } + } + + ( + Written::Nothing, + AcceptorState::LicensingExchange { + early_capability, + channels, + }, + ) + } + + AcceptorState::LicensingExchange { + early_capability, + channels, + } => { + let license: LicensePdu = LicensingErrorMessage::new_valid_client() + .map_err(ConnectorError::encode)? + .into(); + + debug!(message = ?license, "Send"); + + let written = + util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &license, output)?; + + self.saved_for_reactivation = AcceptorState::CapabilitiesSendServer { + early_capability, + channels: channels.clone(), + }; + + ( + Written::from_size(written)?, + AcceptorState::CapabilitiesSendServer { + early_capability, + channels, + }, + ) + } + + AcceptorState::CapabilitiesSendServer { + early_capability, + channels, + } => { + let demand_active = rdp::headers::ShareControlHeader { + share_id: 0, + pdu_source: self.io_channel_id, + share_control_pdu: ShareControlPdu::ServerDemandActive(rdp::capability_sets::ServerDemandActive { + pdu: rdp::capability_sets::DemandActive { + source_descriptor: "".into(), + capability_sets: self.server_capabilities.clone(), + }, + }), + }; + + debug!(message = ?demand_active, "Send"); + + let written = util::encode_send_data_indication( + self.user_channel_id, + self.io_channel_id, + &demand_active, + output, + )?; + + let layout_flag = gcc::ClientEarlyCapabilityFlags::SUPPORT_MONITOR_LAYOUT_PDU; + let next_state = if early_capability.is_some_and(|c| c.contains(layout_flag)) { + AcceptorState::MonitorLayoutSend { channels } + } else { + AcceptorState::CapabilitiesWaitConfirm { channels } + }; + + (Written::from_size(written)?, next_state) + } + + AcceptorState::MonitorLayoutSend { channels } => { + let monitor_layout = + rdp::headers::ShareDataPdu::MonitorLayout(rdp::finalization_messages::MonitorLayoutPdu { + monitors: vec![gcc::Monitor { + left: 0, + top: 0, + right: i32::from(self.desktop_size.width), + bottom: i32::from(self.desktop_size.height), + flags: gcc::MonitorFlags::PRIMARY, + }], + }); + + debug!(message = ?monitor_layout, "Send"); + + let share_data = wrap_share_data(monitor_layout, self.io_channel_id); + + let written = + util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?; + + ( + Written::from_size(written)?, + AcceptorState::CapabilitiesWaitConfirm { channels }, + ) + } + + AcceptorState::CapabilitiesWaitConfirm { ref channels } => { + let message = decode::>>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0); + let message = match message { + Ok(msg) => msg, + Err(e) => { + if self.reactivation { + debug!("Dropping unexpected PDU during reactivation"); + self.state = prev_state; + return Ok(Written::Nothing); + } else { + return Err(e); + } + } + }; + match message { + mcs::McsMessage::SendDataRequest(data) => { + let capabilities_confirm = decode::(data.user_data.as_ref()) + .map_err(ConnectorError::decode); + let capabilities_confirm = match capabilities_confirm { + Ok(capabilities_confirm) => capabilities_confirm, + Err(e) => { + if self.reactivation { + debug!("Dropping unexpected PDU during reactivation"); + self.state = prev_state; + return Ok(Written::Nothing); + } else { + return Err(e); + } + } + }; + + debug!(message = ?capabilities_confirm, "Received"); + + let ShareControlPdu::ClientConfirmActive(confirm) = capabilities_confirm.share_control_pdu + else { + return Err(ConnectorError::general("expected client confirm active")); + }; + + ( + Written::Nothing, + AcceptorState::ConnectionFinalization { + channels: channels.clone(), + finalization: FinalizationSequence::new(self.user_channel_id, self.io_channel_id), + client_capabilities: confirm.pdu.capability_sets, + }, + ) + } + + mcs::McsMessage::DisconnectProviderUltimatum(ultimatum) => { + return Err(reason_err!("received disconnect ultimatum", "{:?}", ultimatum.reason)) + } + + _ => { + warn!(?message, "Unexpected MCS message received"); + + (Written::Nothing, prev_state) + } + } + } + + AcceptorState::ConnectionFinalization { + mut finalization, + channels, + client_capabilities, + } => { + let written = finalization.step(input, output)?; + + let state = if finalization.is_done() { + AcceptorState::Accepted { + channels, + client_capabilities, + input_events: finalization.into_input_events(), + } + } else { + AcceptorState::ConnectionFinalization { + finalization, + channels, + client_capabilities, + } + }; + + (written, state) + } + + _ => unreachable!(), + }; + + self.state = next_state; + Ok(written) + } +} + +fn create_gcc_blocks( + io_channel: u16, + channel_ids: Vec, + requested: SecurityProtocol, + skip_channel_join: bool, +) -> gcc::ServerGccBlocks { + gcc::ServerGccBlocks { + core: gcc::ServerCoreData { + version: gcc::RdpVersion::V5_PLUS, + optional_data: gcc::ServerCoreOptionalData { + client_requested_protocols: Some(requested), + early_capability_flags: skip_channel_join + .then_some(gcc::ServerEarlyCapabilityFlags::SKIP_CHANNELJOIN_SUPPORTED), + }, + }, + security: gcc::ServerSecurityData::no_security(), + network: gcc::ServerNetworkData { + channel_ids, + io_channel, + }, + message_channel: None, + multi_transport_channel: None, + } +} diff --git a/crates/ironrdp-acceptor/src/credssp.rs b/crates/ironrdp-acceptor/src/credssp.rs new file mode 100644 index 00000000..cf50ab56 --- /dev/null +++ b/crates/ironrdp-acceptor/src/credssp.rs @@ -0,0 +1,184 @@ +use ironrdp_async::NetworkClient; +use ironrdp_connector::sspi::credssp::{ + CredSspServer, CredentialsProxy, ServerError, ServerMode, ServerState, TsRequest, +}; +use ironrdp_connector::sspi::generator::{Generator, GeneratorState}; +use ironrdp_connector::sspi::negotiate::ProtocolConfig; +use ironrdp_connector::sspi::{self, AuthIdentity, KerberosServerConfig, NegotiateConfig, NetworkRequest, Username}; +use ironrdp_connector::{ + custom_err, general_err, ConnectorError, ConnectorErrorKind, ConnectorResult, ServerName, Written, +}; +use ironrdp_core::{other_err, WriteBuf}; +use ironrdp_pdu::PduHint; +use tracing::debug; + +#[derive(Debug)] +pub(crate) enum CredsspState { + Ongoing, + Finished, + ServerError(sspi::Error), +} + +#[derive(Clone, Copy, Debug)] +struct CredsspTsRequestHint; + +const CREDSSP_TS_REQUEST_HINT: CredsspTsRequestHint = CredsspTsRequestHint; + +impl PduHint for CredsspTsRequestHint { + fn find_size(&self, bytes: &[u8]) -> ironrdp_core::DecodeResult> { + match TsRequest::read_length(bytes) { + Ok(length) => Ok(Some((true, length))), + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None), + Err(e) => Err(other_err!("CredsspTsRequestHint", source: e)), + } + } +} + +pub type CredsspProcessGenerator<'a> = + Generator<'a, NetworkRequest, sspi::Result>, Result>; + +#[derive(Debug)] +pub struct CredsspSequence<'a> { + server: CredSspServer>, + state: CredsspState, +} + +#[derive(Debug)] +struct CredentialsProxyImpl<'a> { + credentials: &'a AuthIdentity, +} + +impl<'a> CredentialsProxyImpl<'a> { + fn new(credentials: &'a AuthIdentity) -> Self { + Self { credentials } + } +} + +impl CredentialsProxy for CredentialsProxyImpl<'_> { + type AuthenticationData = AuthIdentity; + + fn auth_data_by_user(&mut self, username: &Username) -> std::io::Result { + if username.account_name() != self.credentials.username.account_name() { + return Err(std::io::Error::other("invalid username")); + } + + let mut data = self.credentials.clone(); + // keep the original user/domain + data.username = username.clone(); + Ok(data) + } +} + +pub(crate) async fn resolve_generator( + generator: &mut CredsspProcessGenerator<'_>, + network_client: &mut impl NetworkClient, +) -> Result { + let mut state = generator.start(); + + loop { + match state { + GeneratorState::Suspended(request) => { + let response = network_client.send(&request).await.map_err(|err| ServerError { + ts_request: None, + error: sspi::Error::new(sspi::ErrorKind::InternalError, err), + })?; + state = generator.resume(Ok(response)); + } + GeneratorState::Completed(client_state) => break client_state, + } + } +} + +impl<'a> CredsspSequence<'a> { + pub fn next_pdu_hint(&self) -> ConnectorResult> { + match &self.state { + CredsspState::Ongoing => Ok(Some(&CREDSSP_TS_REQUEST_HINT)), + CredsspState::Finished => Ok(None), + CredsspState::ServerError(err) => Err(custom_err!("Credssp server error", err.clone())), + } + } + + pub fn init( + creds: &'a AuthIdentity, + client_computer_name: ServerName, + public_key: Vec, + krb_config: Option, + ) -> ConnectorResult { + let client_computer_name = client_computer_name.into_inner(); + let credentials = CredentialsProxyImpl::new(creds); + + let credssp_config: Box = if let Some(krb_config) = krb_config { + Box::new(krb_config) + } else { + Box::::default() + }; + + let server = CredSspServer::new( + public_key, + credentials, + ServerMode::Negotiate(NegotiateConfig { + protocol_config: credssp_config, + package_list: None, + client_computer_name, + }), + ) + .map_err(|e| ConnectorError::new("CredSSP", ConnectorErrorKind::Credssp(e)))?; + + let sequence = Self { + server, + state: CredsspState::Ongoing, + }; + + Ok(sequence) + } + + /// Returns Some(ts_request) when a TS request is received from client, + pub fn decode_client_message(&mut self, input: &[u8]) -> ConnectorResult> { + match self.state { + CredsspState::Ongoing => { + let message = TsRequest::from_buffer(input).map_err(|e| custom_err!("TsRequest", e))?; + debug!(?message, "Received"); + Ok(Some(message)) + } + _ => Err(general_err!( + "attempted to feed client request to CredSSP sequence in an unexpected state" + )), + } + } + + pub fn process_ts_request(&mut self, request: TsRequest) -> CredsspProcessGenerator<'_> { + self.server.process(request) + } + + pub fn handle_process_result( + &mut self, + result: Result, + output: &mut WriteBuf, + ) -> ConnectorResult { + let (ts_request, next_state) = match result { + Ok(ServerState::ReplyNeeded(ts_request)) => (Some(ts_request), CredsspState::Ongoing), + Ok(ServerState::Finished(_id)) => (None, CredsspState::Finished), + Err(err) => ( + err.ts_request.map(|ts_request| *ts_request), + CredsspState::ServerError(err.error), + ), + }; + + self.state = next_state; + if let Some(ts_request) = ts_request { + debug!(?ts_request, "Send"); + let length = usize::from(ts_request.buffer_len()); + let unfilled_buffer = output.unfilled_to(length); + + ts_request + .encode_ts_request(unfilled_buffer) + .map_err(|e| custom_err!("TsRequest", e))?; + + output.advance(length); + + Ok(Written::from_size(length)?) + } else { + Ok(Written::Nothing) + } + } +} diff --git a/crates/ironrdp-acceptor/src/finalization.rs b/crates/ironrdp-acceptor/src/finalization.rs new file mode 100644 index 00000000..9961e87c --- /dev/null +++ b/crates/ironrdp-acceptor/src/finalization.rs @@ -0,0 +1,249 @@ +use ironrdp_connector::{ConnectorError, ConnectorErrorExt as _, ConnectorResult, Sequence, State, Written}; +use ironrdp_core::WriteBuf; +use ironrdp_pdu::rdp; +use ironrdp_pdu::x224::X224; +use tracing::debug; + +use crate::util::{self, wrap_share_data}; + +#[derive(Debug)] +pub struct FinalizationSequence { + state: FinalizationState, + user_channel_id: u16, + io_channel_id: u16, + + input_events: Vec>, +} + +#[derive(Default, Debug)] +pub enum FinalizationState { + #[default] + Consumed, + + WaitSynchronize, + WaitControlCooperate, + WaitRequestControl, + WaitFontList, + + SendSynchronizeConfirm, + SendControlCooperateConfirm, + SendGrantedControlConfirm, + SendFontMap, + + Finished, +} + +impl State for FinalizationState { + fn name(&self) -> &'static str { + match self { + Self::Consumed => "Consumed", + Self::WaitSynchronize => "WaitSynchronize", + Self::WaitControlCooperate => "WaitControlCooperate", + Self::WaitRequestControl => "WaitRequestControl", + Self::WaitFontList => "WaitFontList", + Self::SendSynchronizeConfirm => "SendSynchronizeConfirm", + Self::SendControlCooperateConfirm => "SendControlCooperateConfirm", + Self::SendGrantedControlConfirm => "SendGrantedControlConfirm", + Self::SendFontMap => "SendFontMap", + Self::Finished => "Finished", + } + } + + fn is_terminal(&self) -> bool { + matches!(self, Self::Finished { .. }) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +impl Sequence for FinalizationSequence { + fn next_pdu_hint(&self) -> Option<&dyn ironrdp_pdu::PduHint> { + match &self.state { + FinalizationState::Consumed => None, + FinalizationState::WaitSynchronize => Some(&ironrdp_pdu::X224Hint), + FinalizationState::WaitControlCooperate => Some(&ironrdp_pdu::X224Hint), + FinalizationState::WaitRequestControl => Some(&ironrdp_pdu::X224Hint), + FinalizationState::WaitFontList => Some(&ironrdp_pdu::RdpHint), + FinalizationState::SendSynchronizeConfirm => None, + FinalizationState::SendControlCooperateConfirm => None, + FinalizationState::SendGrantedControlConfirm => None, + FinalizationState::SendFontMap => None, + FinalizationState::Finished => None, + } + } + + fn state(&self) -> &dyn State { + &self.state + } + + fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult { + let (written, next_state) = match core::mem::take(&mut self.state) { + FinalizationState::WaitSynchronize => { + let synchronize = decode_share_control(input); + + debug!(message = ?synchronize, "Received"); + + (Written::Nothing, FinalizationState::WaitControlCooperate) + } + + FinalizationState::WaitControlCooperate => { + let cooperate = decode_share_control(input); + + debug!(message = ?cooperate, "Received"); + + (Written::Nothing, FinalizationState::WaitRequestControl) + } + + FinalizationState::WaitRequestControl => { + let control = decode_share_control(input)?; + + debug!(message = ?control, "Received"); + + (Written::Nothing, FinalizationState::WaitFontList) + } + + FinalizationState::WaitFontList => match decode_font_list(input) { + Ok(font_list) => { + debug!(message = ?font_list, "Received"); + + (Written::Nothing, FinalizationState::SendSynchronizeConfirm) + } + + Err(()) => { + self.input_events.push(input.to_vec()); + + (Written::Nothing, FinalizationState::WaitFontList) + } + }, + + FinalizationState::SendSynchronizeConfirm => { + let synchronize_confirm = create_synchronize_confirm(); + + debug!(message = ?synchronize_confirm, "Send"); + + let share_data = wrap_share_data(synchronize_confirm, self.io_channel_id); + let written = + util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?; + + ( + Written::from_size(written)?, + FinalizationState::SendControlCooperateConfirm, + ) + } + + FinalizationState::SendControlCooperateConfirm => { + let cooperate_confirm = create_cooperate_confirm(); + + debug!(message = ?cooperate_confirm, "Send"); + + let share_data = wrap_share_data(cooperate_confirm, self.io_channel_id); + let written = + util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?; + + ( + Written::from_size(written)?, + FinalizationState::SendGrantedControlConfirm, + ) + } + + FinalizationState::SendGrantedControlConfirm => { + let control_confirm = create_control_confirm(self.user_channel_id); + + debug!(message = ?control_confirm, "Send"); + + let share_data = wrap_share_data(control_confirm, self.io_channel_id); + let written = + util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?; + + (Written::from_size(written)?, FinalizationState::SendFontMap) + } + + FinalizationState::SendFontMap => { + let font_map = create_font_map(); + + debug!(message = ?font_map, "Send"); + + let share_data = wrap_share_data(font_map, self.io_channel_id); + let written = + util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?; + + (Written::from_size(written)?, FinalizationState::Finished) + } + + _ => unreachable!(), + }; + + self.state = next_state; + Ok(written) + } +} + +impl FinalizationSequence { + pub fn new(user_channel_id: u16, io_channel_id: u16) -> Self { + Self { + state: FinalizationState::WaitSynchronize, + user_channel_id, + io_channel_id, + input_events: Vec::new(), + } + } + + pub fn into_input_events(self) -> Vec> { + self.input_events + } + + pub fn is_done(&self) -> bool { + self.state.is_terminal() + } +} + +fn create_synchronize_confirm() -> rdp::headers::ShareDataPdu { + rdp::headers::ShareDataPdu::Synchronize(rdp::finalization_messages::SynchronizePdu { target_user_id: 0 }) +} + +fn create_cooperate_confirm() -> rdp::headers::ShareDataPdu { + rdp::headers::ShareDataPdu::Control(rdp::finalization_messages::ControlPdu { + action: rdp::finalization_messages::ControlAction::Cooperate, + grant_id: 0, + control_id: 0, + }) +} + +fn create_control_confirm(user_id: u16) -> rdp::headers::ShareDataPdu { + rdp::headers::ShareDataPdu::Control(rdp::finalization_messages::ControlPdu { + action: rdp::finalization_messages::ControlAction::GrantedControl, + grant_id: user_id, + control_id: u32::from(rdp::capability_sets::SERVER_CHANNEL_ID), + }) +} + +fn create_font_map() -> rdp::headers::ShareDataPdu { + rdp::headers::ShareDataPdu::FontMap(rdp::finalization_messages::FontPdu::default()) +} + +fn decode_share_control(input: &[u8]) -> ConnectorResult { + let data_request = ironrdp_core::decode::>>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + let share_control = ironrdp_core::decode::(data_request.user_data.as_ref()) + .map_err(ConnectorError::decode)?; + Ok(share_control) +} + +fn decode_font_list(input: &[u8]) -> Result { + use ironrdp_pdu::rdp::headers::{ShareControlPdu, ShareDataPdu}; + + let share_control = decode_share_control(input).map_err(|_| ())?; + + let ShareControlPdu::Data(data_pdu) = share_control.share_control_pdu else { + return Err(()); + }; + + let ShareDataPdu::FontList(font_pdu) = data_pdu.share_data_pdu else { + return Err(()); + }; + + Ok(font_pdu) +} diff --git a/crates/ironrdp-acceptor/src/lib.rs b/crates/ironrdp-acceptor/src/lib.rs new file mode 100644 index 00000000..32bc2710 --- /dev/null +++ b/crates/ironrdp-acceptor/src/lib.rs @@ -0,0 +1,225 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +use ironrdp_async::{single_sequence_step, Framed, FramedRead, FramedWrite, NetworkClient, StreamWrapper}; +use ironrdp_connector::sspi::credssp::EarlyUserAuthResult; +use ironrdp_connector::sspi::{AuthIdentity, KerberosServerConfig, Username}; +use ironrdp_connector::{custom_err, general_err, ConnectorResult, ServerName}; +use ironrdp_core::WriteBuf; +use tracing::{debug, instrument, trace}; + +mod channel_connection; +mod connection; +pub mod credssp; +mod finalization; +mod util; + +pub use ironrdp_connector::DesktopSize; +use ironrdp_pdu::nego; + +pub use self::channel_connection::{ChannelConnectionSequence, ChannelConnectionState}; +pub use self::connection::{Acceptor, AcceptorResult, AcceptorState}; +pub use self::finalization::{FinalizationSequence, FinalizationState}; +use crate::credssp::resolve_generator; + +pub enum BeginResult +where + S: StreamWrapper, +{ + ShouldUpgrade(S::InnerStream), + Continue(Framed), +} + +pub async fn accept_begin(mut framed: Framed, acceptor: &mut Acceptor) -> ConnectorResult> +where + S: FramedRead + FramedWrite + StreamWrapper, +{ + let mut buf = WriteBuf::new(); + + loop { + if let Some(security) = acceptor.reached_security_upgrade() { + let result = if security.is_empty() { + BeginResult::Continue(framed) + } else { + BeginResult::ShouldUpgrade(framed.into_inner_no_leftover()) + }; + + return Ok(result); + } + + single_sequence_step(&mut framed, acceptor, &mut buf).await?; + } +} + +pub async fn accept_credssp( + framed: &mut Framed, + acceptor: &mut Acceptor, + network_client: &mut N, + client_computer_name: ServerName, + public_key: Vec, + kerberos_config: Option, +) -> ConnectorResult<()> +where + S: FramedRead + FramedWrite, + N: NetworkClient, +{ + let mut buf = WriteBuf::new(); + + if acceptor.should_perform_credssp() { + perform_credssp_step( + framed, + acceptor, + network_client, + &mut buf, + client_computer_name, + public_key, + kerberos_config, + ) + .await + } else { + Ok(()) + } +} + +pub async fn accept_finalize( + mut framed: Framed, + acceptor: &mut Acceptor, +) -> ConnectorResult<(Framed, AcceptorResult)> +where + S: FramedRead + FramedWrite, +{ + let mut buf = WriteBuf::new(); + + loop { + if let Some(result) = acceptor.get_result() { + return Ok((framed, result)); + } + single_sequence_step(&mut framed, acceptor, &mut buf).await?; + } +} + +#[instrument(level = "trace", skip_all, ret)] +async fn perform_credssp_step( + framed: &mut Framed, + acceptor: &mut Acceptor, + network_client: &mut N, + buf: &mut WriteBuf, + client_computer_name: ServerName, + public_key: Vec, + kerberos_config: Option, +) -> ConnectorResult<()> +where + S: FramedRead + FramedWrite, + N: NetworkClient, +{ + assert!(acceptor.should_perform_credssp()); + let AcceptorState::Credssp { protocol, .. } = acceptor.state else { + unreachable!() + }; + + let result = credssp_loop( + framed, + acceptor, + network_client, + buf, + client_computer_name, + public_key, + kerberos_config, + ) + .await; + + if protocol.intersects(nego::SecurityProtocol::HYBRID_EX) { + trace!(?result, "HYBRID_EX"); + + let result = if result.is_ok() { + EarlyUserAuthResult::Success + } else { + EarlyUserAuthResult::AccessDenied + }; + + buf.clear(); + result + .to_buffer(&mut *buf) + .map_err(|e| ironrdp_connector::custom_err!("to_buffer", e))?; + let response = &buf[..result.buffer_len()]; + framed + .write_all(response) + .await + .map_err(|e| ironrdp_connector::custom_err!("write all", e))?; + } + + result?; + + acceptor.mark_credssp_as_done(); + + return Ok(()); + + async fn credssp_loop( + framed: &mut Framed, + acceptor: &mut Acceptor, + network_client: &mut N, + buf: &mut WriteBuf, + client_computer_name: ServerName, + public_key: Vec, + kerberos_config: Option, + ) -> ConnectorResult<()> + where + S: FramedRead + FramedWrite, + N: NetworkClient, + { + let creds = acceptor + .creds + .as_ref() + .ok_or_else(|| general_err!("no credentials while doing credssp"))?; + let username = Username::new(&creds.username, None).map_err(|e| custom_err!("invalid username", e))?; + let identity = AuthIdentity { + username, + password: creds.password.clone().into(), + }; + + let mut sequence = + credssp::CredsspSequence::init(&identity, client_computer_name, public_key, kerberos_config)?; + + loop { + let Some(next_pdu_hint) = sequence.next_pdu_hint()? else { + break; + }; + + debug!( + acceptor.state = ?acceptor.state, + hint = ?next_pdu_hint, + "Wait for PDU" + ); + + let pdu = framed + .read_by_hint(next_pdu_hint) + .await + .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; + + trace!(length = pdu.len(), "PDU received"); + + let Some(ts_request) = sequence.decode_client_message(&pdu)? else { + break; + }; + + let result = { + let mut generator = sequence.process_ts_request(ts_request); + resolve_generator(&mut generator, network_client).await + }; // drop generator + + buf.clear(); + let written = sequence.handle_process_result(result, buf)?; + + if let Some(response_len) = written.size() { + let response = &buf[..response_len]; + trace!(response_len, "Send response"); + framed + .write_all(response) + .await + .map_err(|e| ironrdp_connector::custom_err!("write all", e))?; + } + } + + Ok(()) + } +} diff --git a/crates/ironrdp-acceptor/src/util.rs b/crates/ironrdp-acceptor/src/util.rs new file mode 100644 index 00000000..0b22a8c1 --- /dev/null +++ b/crates/ironrdp-acceptor/src/util.rs @@ -0,0 +1,41 @@ +use std::borrow::Cow; + +use ironrdp_connector::{ConnectorError, ConnectorErrorExt as _, ConnectorResult}; +use ironrdp_core::{encode_vec, Encode, WriteBuf}; +use ironrdp_pdu::rdp; +use ironrdp_pdu::x224::X224; + +pub(crate) fn encode_send_data_indication( + initiator_id: u16, + channel_id: u16, + user_msg: &T, + buf: &mut WriteBuf, +) -> ConnectorResult +where + T: Encode, +{ + let user_data = encode_vec(user_msg).map_err(ConnectorError::encode)?; + + let pdu = ironrdp_pdu::mcs::SendDataIndication { + initiator_id, + channel_id, + user_data: Cow::Owned(user_data), + }; + + let written = ironrdp_core::encode_buf(&X224(pdu), buf).map_err(ConnectorError::encode)?; + + Ok(written) +} + +pub(crate) fn wrap_share_data(pdu: rdp::headers::ShareDataPdu, io_channel_id: u16) -> rdp::headers::ShareControlHeader { + rdp::headers::ShareControlHeader { + share_id: 0, + pdu_source: io_channel_id, + share_control_pdu: rdp::headers::ShareControlPdu::Data(rdp::headers::ShareDataHeader { + share_data_pdu: pdu, + stream_priority: rdp::headers::StreamPriority::Undefined, + compression_flags: rdp::headers::CompressionFlags::empty(), + compression_type: rdp::client_info::CompressionType::K8, + }), + } +} diff --git a/crates/ironrdp-ainput/CHANGELOG.md b/crates/ironrdp-ainput/CHANGELOG.md new file mode 100644 index 00000000..188f0c3e --- /dev/null +++ b/crates/ironrdp-ainput/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.2.0...ironrdp-ainput-v0.2.1)] - 2025-05-27 + +### Build + +- Bump bitflags from 2.9.0 to 2.9.1 in the patch group across 1 directory (#792) ([87ed315bc2](https://github.com/Devolutions/IronRDP/commit/87ed315bc28fdd2dcfea89b052fa620a7e346e5a)) + + + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.1.2...ironrdp-ainput-v0.1.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.1.1...ironrdp-ainput-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.1.0...ironrdp-ainput-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-ainput/Cargo.toml b/crates/ironrdp-ainput/Cargo.toml new file mode 100644 index 00000000..f2e4037a --- /dev/null +++ b/crates/ironrdp-ainput/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ironrdp-ainput" +version = "0.4.0" +readme = "README.md" +description = "AInput dynamic channel implementation" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.4" } # public +bitflags = "2.9" +num-derive.workspace = true # TODO: remove +num-traits.workspace = true # TODO: remove + +[lints] +workspace = true + diff --git a/crates/ironrdp-ainput/LICENSE-APACHE b/crates/ironrdp-ainput/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-ainput/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-ainput/LICENSE-MIT b/crates/ironrdp-ainput/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-ainput/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-ainput/README.md b/crates/ironrdp-ainput/README.md new file mode 100644 index 00000000..71c85114 --- /dev/null +++ b/crates/ironrdp-ainput/README.md @@ -0,0 +1,8 @@ +# IronRDP AInput + +Implements the "Advanced Input" dynamic channel as defined from [Freerdp][here]. + +This crate is part of the [IronRDP] project. + +[here]: https://github.com/FreeRDP/FreeRDP/blob/master/include/freerdp/channels/ainput.h +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-ainput/src/lib.rs b/crates/ironrdp-ainput/src/lib.rs new file mode 100644 index 00000000..eba9c469 --- /dev/null +++ b/crates/ironrdp-ainput/src/lib.rs @@ -0,0 +1,290 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use ironrdp_dvc::DvcEncode; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; +// Advanced Input channel as defined from Freerdp, [here]: +// +// [here]: https://github.com/FreeRDP/FreeRDP/blob/master/include/freerdp/channels/ainput.h + +const VERSION_MAJOR: u32 = 1; +const VERSION_MINOR: u32 = 0; + +pub const CHANNEL_NAME: &str = "FreeRDP::Advanced::Input"; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct MouseEventFlags: u64 { + const WHEEL = 0x0000_0001; + const MOVE = 0x0000_0004; + const DOWN = 0x0000_0008; + + const REL = 0x0000_0010; + const HAVE_REL = 0x0000_0020; + const BUTTON1 = 0x0000_1000; /* left */ + const BUTTON2 = 0x0000_2000; /* right */ + const BUTTON3 = 0x0000_4000; /* middle */ + + const XBUTTON1 = 0x0000_0100; + const XBUTTON2 = 0x0000_0200; + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VersionPdu { + major_version: u32, + minor_version: u32, +} + +impl VersionPdu { + const NAME: &'static str = "AInputVersionPdu"; + + const FIXED_PART_SIZE: usize = 4 /* MajorVersion */ + 4 /* MinorVersion */; + + pub fn new() -> Self { + Self { + major_version: VERSION_MAJOR, + minor_version: VERSION_MINOR, + } + } +} + +impl Default for VersionPdu { + fn default() -> Self { + Self::new() + } +} + +impl Encode for VersionPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.major_version); + dst.write_u32(self.minor_version); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for VersionPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let major_version = src.read_u32(); + let minor_version = src.read_u32(); + + Ok(Self { + major_version, + minor_version, + }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(u16)] +pub enum ServerPduType { + Version = 0x01, +} + +impl ServerPduType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(&self) -> u16 { + *self as u16 + } +} + +impl<'a> From<&'a ServerPdu> for ServerPduType { + fn from(s: &'a ServerPdu) -> Self { + match s { + ServerPdu::Version(_) => Self::Version, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ServerPdu { + Version(VersionPdu), +} + +impl ServerPdu { + const NAME: &'static str = "AInputServerPdu"; + + const FIXED_PART_SIZE: usize = 2 /* PduType */; +} + +impl Encode for ServerPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(ServerPduType::from(self).as_u16()); + match self { + ServerPdu::Version(pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(match self { + ServerPdu::Version(pdu) => pdu.size(), + }) + .expect("never overflow") + } +} + +impl DvcEncode for ServerPdu {} + +impl<'de> Decode<'de> for ServerPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let pdu_type = + ServerPduType::from_u16(src.read_u16()).ok_or_else(|| invalid_field_err!("pduType", "invalid pdu type"))?; + + let server_pdu = match pdu_type { + ServerPduType::Version => ServerPdu::Version(VersionPdu::decode(src)?), + }; + + Ok(server_pdu) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MousePdu { + pub time: u64, + pub flags: MouseEventFlags, + pub x: i32, + pub y: i32, +} + +impl MousePdu { + const NAME: &'static str = "AInputMousePdu"; + + const FIXED_PART_SIZE: usize = 8 /* Time */ + 8 /* Flags */ + 4 /* X */ + 4 /* Y */; +} + +impl Encode for MousePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u64(self.time); + dst.write_u64(self.flags.bits()); + dst.write_i32(self.x); + dst.write_i32(self.y); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for MousePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let time = src.read_u64(); + let flags = MouseEventFlags::from_bits_retain(src.read_u64()); + let x = src.read_i32(); + let y = src.read_i32(); + + Ok(Self { time, flags, x, y }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ClientPdu { + Mouse(MousePdu), +} + +impl ClientPdu { + const NAME: &'static str = "AInputClientPdu"; + + const FIXED_PART_SIZE: usize = 2 /* PduType */; +} + +impl Encode for ClientPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(ClientPduType::from(self).as_u16()); + match self { + ClientPdu::Mouse(pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(match self { + ClientPdu::Mouse(pdu) => pdu.size(), + }) + .expect("never overflow") + } +} + +impl<'de> Decode<'de> for ClientPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let pdu_type = + ClientPduType::from_u16(src.read_u16()).ok_or_else(|| invalid_field_err!("pduType", "invalid pdu type"))?; + + let client_pdu = match pdu_type { + ClientPduType::Mouse => ClientPdu::Mouse(MousePdu::decode(src)?), + }; + + Ok(client_pdu) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(u16)] +pub enum ClientPduType { + Mouse = 0x02, +} + +impl ClientPduType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +impl<'a> From<&'a ClientPdu> for ClientPduType { + fn from(s: &'a ClientPdu) -> Self { + match s { + ClientPdu::Mouse(_) => Self::Mouse, + } + } +} diff --git a/crates/ironrdp-async/CHANGELOG.md b/crates/ironrdp-async/CHANGELOG.md new file mode 100644 index 00000000..d98372c7 --- /dev/null +++ b/crates/ironrdp-async/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.7.0...ironrdp-async-v0.8.0)] - 2025-12-18 + +### Bug Fixes + +- [**breaking**] Use static dispatch for NetworkClient trait ([#1043](https://github.com/Devolutions/IronRDP/issues/1043)) ([bca6d190a8](https://github.com/Devolutions/IronRDP/commit/bca6d190a870708468534d224ff225a658767a9a)) + + - Rename `AsyncNetworkClient` to `NetworkClient` + - Replace dynamic dispatch (`Option<&mut dyn ...>`) with static dispatch + using generics (`&mut N where N: NetworkClient`) + - Reorder `connect_finalize` parameters for consistency across crates + +## [[0.3.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.3.1...ironrdp-async-v0.3.2)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.3.0...ironrdp-async-v0.3.1)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.2.1...ironrdp-async-v0.3.0)] - 2025-01-28 + +### Changed + +- Remove unmatched parameter from `Framed::read_by_hint` function ([63963182b5](https://github.com/Devolutions/IronRDP/commit/63963182b5af6ad45dc638e93de4b8a0b565c7d3)) + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.2.0...ironrdp-async-v0.2.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-async/Cargo.toml b/crates/ironrdp-async/Cargo.toml new file mode 100644 index 00000000..1716abb4 --- /dev/null +++ b/crates/ironrdp-async/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ironrdp-async" +version = "0.8.0" +readme = "README.md" +description = "Provides `Future`s wrapping the IronRDP state machines conveniently" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-connector = { path = "../ironrdp-connector", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public +tracing = { version = "0.1", features = ["log"] } +bytes = "1" # public + +[lints] +workspace = true diff --git a/crates/ironrdp-async/LICENSE-APACHE b/crates/ironrdp-async/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-async/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-async/LICENSE-MIT b/crates/ironrdp-async/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-async/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-async/README.md b/crates/ironrdp-async/README.md new file mode 100644 index 00000000..fa2e7234 --- /dev/null +++ b/crates/ironrdp-async/README.md @@ -0,0 +1,7 @@ +# IronRDP Async + +`Future`s built on top of `ironrdp-connector` and `ironrdp-session` crates. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-async/src/connector.rs b/crates/ironrdp-async/src/connector.rs new file mode 100644 index 00000000..04f3d5a7 --- /dev/null +++ b/crates/ironrdp-async/src/connector.rs @@ -0,0 +1,189 @@ +use ironrdp_connector::credssp::{CredsspProcessGenerator, CredsspSequence, KerberosConfig}; +use ironrdp_connector::sspi::credssp::ClientState; +use ironrdp_connector::sspi::generator::GeneratorState; +use ironrdp_connector::{ + general_err, ClientConnector, ClientConnectorState, ConnectionResult, ConnectorError, ConnectorResult, ServerName, + State as _, +}; +use ironrdp_core::WriteBuf; +use tracing::{debug, info, instrument, trace}; + +use crate::framed::{Framed, FramedRead, FramedWrite}; +use crate::{single_sequence_step, NetworkClient}; + +#[non_exhaustive] +pub struct ShouldUpgrade; + +#[instrument(skip_all)] +pub async fn connect_begin(framed: &mut Framed, connector: &mut ClientConnector) -> ConnectorResult +where + S: Sync + FramedRead + FramedWrite, +{ + let mut buf = WriteBuf::new(); + + info!("Begin connection procedure"); + + while !connector.should_perform_security_upgrade() { + single_sequence_step(framed, connector, &mut buf).await?; + } + + Ok(ShouldUpgrade) +} + +/// # Panics +/// +/// Panics if connector state is not [ClientConnectorState::EnhancedSecurityUpgrade]. +pub fn skip_connect_begin(connector: &mut ClientConnector) -> ShouldUpgrade { + assert!(connector.should_perform_security_upgrade()); + ShouldUpgrade +} + +#[non_exhaustive] +pub struct Upgraded; + +#[instrument(skip_all)] +pub fn mark_as_upgraded(_: ShouldUpgrade, connector: &mut ClientConnector) -> Upgraded { + trace!("Marked as upgraded"); + connector.mark_security_upgrade_as_done(); + Upgraded +} + +#[instrument(skip_all)] +pub async fn connect_finalize( + _: Upgraded, + mut connector: ClientConnector, + framed: &mut Framed, + network_client: &mut N, + server_name: ServerName, + server_public_key: Vec, + kerberos_config: Option, +) -> ConnectorResult +where + S: FramedRead + FramedWrite, + N: NetworkClient, +{ + let mut buf = WriteBuf::new(); + + if connector.should_perform_credssp() { + perform_credssp_step( + &mut connector, + framed, + network_client, + &mut buf, + server_name, + server_public_key, + kerberos_config, + ) + .await?; + } + + let result = loop { + single_sequence_step(framed, &mut connector, &mut buf).await?; + + if let ClientConnectorState::Connected { result } = connector.state { + break result; + } + }; + + info!("Connected with success"); + + Ok(result) +} + +async fn resolve_generator( + generator: &mut CredsspProcessGenerator<'_>, + network_client: &mut impl NetworkClient, +) -> ConnectorResult { + let mut state = generator.start(); + + loop { + match state { + GeneratorState::Suspended(request) => { + let response = network_client.send(&request).await?; + state = generator.resume(Ok(response)); + } + GeneratorState::Completed(client_state) => { + break client_state + .map_err(|e| ConnectorError::new("CredSSP", ironrdp_connector::ConnectorErrorKind::Credssp(e))) + } + } + } +} + +#[instrument(level = "trace", skip_all)] +async fn perform_credssp_step( + connector: &mut ClientConnector, + framed: &mut Framed, + network_client: &mut N, + buf: &mut WriteBuf, + server_name: ServerName, + server_public_key: Vec, + kerberos_config: Option, +) -> ConnectorResult<()> +where + S: FramedRead + FramedWrite, + N: NetworkClient, +{ + assert!(connector.should_perform_credssp()); + + let selected_protocol = match connector.state { + ClientConnectorState::Credssp { selected_protocol, .. } => selected_protocol, + _ => return Err(general_err!("invalid connector state for CredSSP sequence")), + }; + + let (mut sequence, mut ts_request) = CredsspSequence::init( + connector.config.credentials.clone(), + connector.config.domain.as_deref(), + selected_protocol, + server_name, + server_public_key, + kerberos_config, + )?; + + loop { + let client_state = { + let mut generator = sequence.process_ts_request(ts_request); + trace!("resolving network"); + resolve_generator(&mut generator, network_client).await? + }; // drop generator + + buf.clear(); + let written = sequence.handle_process_result(client_state, buf)?; + + if let Some(response_len) = written.size() { + let response = &buf[..response_len]; + trace!(response_len, "Send response"); + framed + .write_all(response) + .await + .map_err(|e| ironrdp_connector::custom_err!("write all", e))?; + } + + let Some(next_pdu_hint) = sequence.next_pdu_hint() else { + break; + }; + + debug!( + connector.state = connector.state.name(), + hint = ?next_pdu_hint, + "Wait for PDU" + ); + + let pdu = framed + .read_by_hint(next_pdu_hint) + .await + .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; + + trace!(length = pdu.len(), "PDU received"); + + if let Some(next_request) = sequence.decode_server_message(&pdu)? { + ts_request = next_request; + } else { + break; + } + } + + connector.mark_credssp_as_done(); + + Ok(()) +} diff --git a/crates/ironrdp-async/src/framed.rs b/crates/ironrdp-async/src/framed.rs new file mode 100644 index 00000000..095a8328 --- /dev/null +++ b/crates/ironrdp-async/src/framed.rs @@ -0,0 +1,290 @@ +use std::io; + +use bytes::{Bytes, BytesMut}; +use ironrdp_connector::{ConnectorResult, Sequence, Written}; +use ironrdp_core::WriteBuf; +use ironrdp_pdu::PduHint; +use tracing::{debug, trace}; + +// TODO: investigate if we could use static async fn / return position impl trait in traits when stabilized: +// https://github.com/rust-lang/rust/issues/91611 + +pub trait FramedRead { + type ReadFut<'read>: core::future::Future> + 'read + where + Self: 'read; + + /// Reads from stream and fills internal buffer + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is guaranteed that no data was read. + fn read<'a>(&'a mut self, buf: &'a mut BytesMut) -> Self::ReadFut<'a>; +} + +pub trait FramedWrite { + type WriteAllFut<'write>: core::future::Future> + 'write + where + Self: 'write; + + /// Writes an entire buffer into this stream. + /// + /// # Cancel safety + /// + /// This method is not cancellation safe. If it is used as the event + /// in a `tokio::select!` statement and some other + /// branch completes first, then the provided buffer may have been + /// partially written, but future calls to `write_all` will start over + /// from the beginning of the buffer. + fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a>; +} + +pub trait StreamWrapper: Sized { + type InnerStream; + + fn from_inner(stream: Self::InnerStream) -> Self; + + fn into_inner(self) -> Self::InnerStream; + + fn get_inner(&self) -> &Self::InnerStream; + + fn get_inner_mut(&mut self) -> &mut Self::InnerStream; +} + +pub struct Framed { + stream: S, + buf: BytesMut, +} + +impl Framed { + pub fn peek(&self) -> &[u8] { + &self.buf + } +} + +impl Framed +where + S: StreamWrapper, +{ + pub fn new(stream: S::InnerStream) -> Self { + Self::new_with_leftover(stream, BytesMut::new()) + } + + pub fn new_with_leftover(stream: S::InnerStream, leftover: BytesMut) -> Self { + Self { + stream: S::from_inner(stream), + buf: leftover, + } + } + + pub fn into_inner(self) -> (S::InnerStream, BytesMut) { + (self.stream.into_inner(), self.buf) + } + + pub fn into_inner_no_leftover(self) -> S::InnerStream { + let (stream, leftover) = self.into_inner(); + debug_assert_eq!(leftover.len(), 0, "unexpected leftover"); + stream + } + + pub fn get_inner(&self) -> (&S::InnerStream, &BytesMut) { + (self.stream.get_inner(), &self.buf) + } + + pub fn get_inner_mut(&mut self) -> (&mut S::InnerStream, &mut BytesMut) { + (self.stream.get_inner_mut(), &mut self.buf) + } +} + +impl Framed +where + S: FramedRead, +{ + /// Accumulates at least `length` bytes and returns exactly `length` bytes, keeping the leftover in the internal buffer. + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is safe to drop the future and re-create it later. + /// Data may have been read, but it will be stored in the internal buffer. + pub async fn read_exact(&mut self, length: usize) -> io::Result { + loop { + if self.buf.len() >= length { + return Ok(self.buf.split_to(length)); + } else { + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer underflow)")] + self.buf + .reserve(length.checked_sub(self.buf.len()).expect("length > self.buf.len()")); + } + + let len = self.read().await?; + + // Handle EOF + if len == 0 { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); + } + } + } + + /// Reads a standard RDP PDU frame. + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is safe to drop the future and re-create it later. + /// Data may have been read, but it will be stored in the internal buffer. + pub async fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, BytesMut)> { + loop { + // Try decoding and see if a frame has been received already + match ironrdp_pdu::find_size(self.peek()) { + Ok(Some(pdu_info)) => { + let frame = self.read_exact(pdu_info.length).await?; + + return Ok((pdu_info.action, frame)); + } + Ok(None) => { + let len = self.read().await?; + + // Handle EOF + if len == 0 { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); + } + } + Err(e) => return Err(io::Error::other(e)), + }; + } + } + + /// Reads a frame using the provided PduHint. + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is safe to drop the future and re-create it later. + /// Data may have been read, but it will be stored in the internal buffer. + pub async fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result { + loop { + match hint.find_size(self.peek()).map_err(io::Error::other)? { + Some((matched, length)) => { + let bytes = self.read_exact(length).await?.freeze(); + if matched { + return Ok(bytes); + } else { + debug!("Received and lost an unexpected PDU"); + } + } + None => { + let len = self.read().await?; + + // Handle EOF + if len == 0 { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); + } + } + }; + } + } + + /// Reads from stream and fills internal buffer, returning how many bytes were read. + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is guaranteed that no data was read. + async fn read(&mut self) -> io::Result { + self.stream.read(&mut self.buf).await + } +} + +impl FramedWrite for Framed +where + S: FramedWrite, +{ + type WriteAllFut<'write> + = S::WriteAllFut<'write> + where + Self: 'write; + + /// Attempts to write an entire buffer into this `Framed`’s stream. + /// + /// # Cancel safety + /// + /// This method is not cancellation safe. If it is used as the event + /// in a `tokio::select!` statement and some other + /// branch completes first, then the provided buffer may have been + /// partially written, but future calls to `write_all` will start over + /// from the beginning of the buffer. + fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a> { + self.stream.write_all(buf) + } +} + +pub async fn single_sequence_step( + framed: &mut Framed, + sequence: &mut dyn Sequence, + buf: &mut WriteBuf, +) -> ConnectorResult<()> +where + S: FramedWrite + FramedRead, +{ + buf.clear(); + let written = single_sequence_step_read(framed, sequence, buf).await?; + single_sequence_step_write(framed, buf, written).await +} + +pub async fn single_sequence_step_read( + framed: &mut Framed, + sequence: &mut dyn Sequence, + buf: &mut WriteBuf, +) -> ConnectorResult +where + S: FramedRead, +{ + buf.clear(); + + if let Some(next_pdu_hint) = sequence.next_pdu_hint() { + debug!( + connector.state = sequence.state().name(), + hint = ?next_pdu_hint, + "Wait for PDU" + ); + + let pdu = framed + .read_by_hint(next_pdu_hint) + .await + .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; + + trace!(length = pdu.len(), "PDU received"); + + sequence.step(&pdu, buf) + } else { + sequence.step_no_input(buf) + } +} + +async fn single_sequence_step_write( + framed: &mut Framed, + buf: &mut WriteBuf, + written: Written, +) -> ConnectorResult<()> +where + S: FramedWrite, +{ + if let Some(response_len) = written.size() { + debug_assert_eq!(buf.filled_len(), response_len); + let response = buf.filled(); + trace!(response_len, "Send response"); + framed + .write_all(response) + .await + .map_err(|e| ironrdp_connector::custom_err!("write all", e))?; + } + + Ok(()) +} diff --git a/crates/ironrdp-async/src/lib.rs b/crates/ironrdp-async/src/lib.rs new file mode 100644 index 00000000..847200c0 --- /dev/null +++ b/crates/ironrdp-async/src/lib.rs @@ -0,0 +1,21 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +use core::future::Future; + +pub use bytes; + +mod connector; +mod framed; +mod session; + +use ironrdp_connector::sspi::generator::NetworkRequest; +use ironrdp_connector::ConnectorResult; + +pub use self::connector::*; +pub use self::framed::*; +// pub use self::session::*; + +pub trait NetworkClient { + fn send(&mut self, network_request: &NetworkRequest) -> impl Future>>; +} diff --git a/crates/ironrdp-async/src/session.rs b/crates/ironrdp-async/src/session.rs new file mode 100644 index 00000000..9fa65f77 --- /dev/null +++ b/crates/ironrdp-async/src/session.rs @@ -0,0 +1 @@ +// TODO: active session async helpers diff --git a/crates/ironrdp-bench/Cargo.toml b/crates/ironrdp-bench/Cargo.toml new file mode 100644 index 00000000..ea35d2ce --- /dev/null +++ b/crates/ironrdp-bench/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ironrdp-bench" +version = "0.0.0" +description = "IronRDP benchmarks" +edition.workspace = true +publish = false + +[dev-dependencies] +criterion = "0.8" +ironrdp-graphics.path = "../ironrdp-graphics" +ironrdp-pdu.path = "../ironrdp-pdu" +ironrdp-server = { path = "../ironrdp-server", features = ["__bench"] } + +[[bench]] +name = "bench" +path = "benches/bench.rs" +harness = false + +[lints] +workspace = true diff --git a/crates/ironrdp-bench/benches/bench.rs b/crates/ironrdp-bench/benches/bench.rs new file mode 100644 index 00000000..4dc088ad --- /dev/null +++ b/crates/ironrdp-bench/benches/bench.rs @@ -0,0 +1,80 @@ +#![expect(clippy::missing_panics_doc, reason = "panics in benches are allowed")] + +use core::num::{NonZeroU16, NonZeroUsize}; + +use criterion::{criterion_group, criterion_main, Criterion}; +use ironrdp_graphics::color_conversion::to_64x64_ycbcr_tile; +use ironrdp_pdu::codecs::rfx; +use ironrdp_server::bench::encoder::rfx::{rfx_enc, rfx_enc_tile}; +use ironrdp_server::BitmapUpdate; + +pub fn rfx_enc_tile_bench(c: &mut Criterion) { + const WIDTH: NonZeroU16 = NonZeroU16::new(64).expect("value is guaranteed to be non-zero"); + const HEIGHT: NonZeroU16 = NonZeroU16::new(64).expect("value is guaranteed to be non-zero"); + const STRIDE: NonZeroUsize = NonZeroUsize::new(64 * 4).expect("value is guaranteed to be non-zero"); + + let quant = rfx::Quant::default(); + let algo = rfx::EntropyAlgorithm::Rlgr3; + + let bitmap = BitmapUpdate { + x: 0, + y: 0, + width: WIDTH, + height: HEIGHT, + format: ironrdp_server::PixelFormat::ARgb32, + data: vec![0; 64 * 64 * 4].into(), + stride: STRIDE, + }; + c.bench_function("rfx_enc_tile", |b| b.iter(|| rfx_enc_tile(&bitmap, &quant, algo, 0, 0))); +} + +pub fn rfx_enc_bench(c: &mut Criterion) { + const WIDTH: NonZeroU16 = NonZeroU16::new(2048).expect("value is guaranteed to be non-zero"); + const HEIGHT: NonZeroU16 = NonZeroU16::new(2048).expect("value is guaranteed to be non-zero"); + // FIXME/QUESTION: It looks like we have a bug here, don't we? The stride value should be 2048 * 4. + const STRIDE: NonZeroUsize = NonZeroUsize::new(64 * 4).expect("value is guaranteed to be non-zero"); + + let quant = rfx::Quant::default(); + let algo = rfx::EntropyAlgorithm::Rlgr3; + + let bitmap = BitmapUpdate { + x: 0, + y: 0, + width: WIDTH, + height: HEIGHT, + format: ironrdp_server::PixelFormat::ARgb32, + data: vec![0; 2048 * 2048 * 4].into(), + stride: STRIDE, + }; + c.bench_function("rfx_enc", |b| b.iter(|| rfx_enc(&bitmap, &quant, algo))); +} + +pub fn to_ycbcr_bench(c: &mut Criterion) { + const WIDTH: usize = 64; + const HEIGHT: usize = 64; + + let input = vec![0; WIDTH * HEIGHT * 4]; + let stride = WIDTH * 4; + let mut y = [0i16; WIDTH * HEIGHT]; + let mut cb = [0i16; WIDTH * HEIGHT]; + let mut cr = [0i16; WIDTH * HEIGHT]; + let format = ironrdp_graphics::image_processing::PixelFormat::ARgb32; + + c.bench_function("to_ycbcr", |b| { + b.iter(|| { + to_64x64_ycbcr_tile( + &input, + WIDTH.try_into().expect("can't panic"), + HEIGHT.try_into().expect("can't panic"), + stride.try_into().expect("can't panic"), + format, + &mut y, + &mut cb, + &mut cr, + ) + }) + }); +} + +criterion_group!(benches, rfx_enc_tile_bench, rfx_enc_bench, to_ycbcr_bench); +criterion_main!(benches); diff --git a/crates/ironrdp-blocking/CHANGELOG.md b/crates/ironrdp-blocking/CHANGELOG.md new file mode 100644 index 00000000..02048123 --- /dev/null +++ b/crates/ironrdp-blocking/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.7.0...ironrdp-blocking-v0.8.0)] - 2025-12-18 + +### Bug Fixes + +- [**breaking**] Use static dispatch for NetworkClient trait ([#1043](https://github.com/Devolutions/IronRDP/issues/1043)) ([bca6d190a8](https://github.com/Devolutions/IronRDP/commit/bca6d190a870708468534d224ff225a658767a9a)) + + - Rename `AsyncNetworkClient` to `NetworkClient` + - Replace dynamic dispatch (`Option<&mut dyn ...>`) with static dispatch + using generics (`&mut N where N: NetworkClient`) + - Reorder `connect_finalize` parameters for consistency across crates + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.3.1...ironrdp-blocking-v0.4.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + + +## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.3.0...ironrdp-blocking-v0.3.1)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.2.1...ironrdp-blocking-v0.3.0)] - 2025-01-28 + +### Changed + +- Remove unmatched parameter from `Framed::read_by_hint` function ([63963182b5](https://github.com/Devolutions/IronRDP/commit/63963182b5af6ad45dc638e93de4b8a0b565c7d3)) + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.2.0...ironrdp-blocking-v0.2.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-blocking/Cargo.toml b/crates/ironrdp-blocking/Cargo.toml new file mode 100644 index 00000000..557640c8 --- /dev/null +++ b/crates/ironrdp-blocking/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ironrdp-blocking" +version = "0.8.0" +readme = "README.md" +description = "Blocking I/O abstraction wrapping the IronRDP state machines conveniently" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-connector = { path = "../ironrdp-connector", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public +tracing = { version = "0.1", features = ["log"] } +bytes = "1" # public + +[lints] +workspace = true + diff --git a/crates/ironrdp-blocking/LICENSE-APACHE b/crates/ironrdp-blocking/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-blocking/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-blocking/LICENSE-MIT b/crates/ironrdp-blocking/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-blocking/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-blocking/README.md b/crates/ironrdp-blocking/README.md new file mode 100644 index 00000000..6aa4708f --- /dev/null +++ b/crates/ironrdp-blocking/README.md @@ -0,0 +1,11 @@ +# IronRDP Blocking + +Blocking I/O abstraction wrapping the IronRDP state machines conveniently. + +This crate is a higher level abstraction for IronRDP state machines using blocking I/O instead of +asynchronous I/O. This results in a simpler API with fewer dependencies that may be used +instead of `ironrdp-async` when concurrency is not a requirement. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-blocking/src/connector.rs b/crates/ironrdp-blocking/src/connector.rs new file mode 100644 index 00000000..b50805ae --- /dev/null +++ b/crates/ironrdp-blocking/src/connector.rs @@ -0,0 +1,230 @@ +use std::io::{Read, Write}; + +use ironrdp_connector::credssp::{CredsspProcessGenerator, CredsspSequence, KerberosConfig}; +use ironrdp_connector::sspi::credssp::ClientState; +use ironrdp_connector::sspi::generator::GeneratorState; +use ironrdp_connector::sspi::network_client::NetworkClient; +use ironrdp_connector::{ + general_err, ClientConnector, ClientConnectorState, ConnectionResult, ConnectorError, ConnectorResult, + Sequence as _, ServerName, State as _, +}; +use ironrdp_core::WriteBuf; +use tracing::{debug, info, instrument, trace}; + +use crate::framed::Framed; + +#[non_exhaustive] +pub struct ShouldUpgrade; + +#[instrument(skip_all)] +pub fn connect_begin(framed: &mut Framed, connector: &mut ClientConnector) -> ConnectorResult +where + S: Sync + Read + Write, +{ + let mut buf = WriteBuf::new(); + + info!("Begin connection procedure"); + + while !connector.should_perform_security_upgrade() { + single_sequence_step(framed, connector, &mut buf)?; + } + + Ok(ShouldUpgrade) +} + +/// # Panics +/// +/// Panics if connector state is not [ClientConnectorState::EnhancedSecurityUpgrade]. +pub fn skip_connect_begin(connector: &mut ClientConnector) -> ShouldUpgrade { + assert!(connector.should_perform_security_upgrade()); + ShouldUpgrade +} + +#[non_exhaustive] +pub struct Upgraded; + +#[instrument(skip_all)] +pub fn mark_as_upgraded(_: ShouldUpgrade, connector: &mut ClientConnector) -> Upgraded { + trace!("Marked as upgraded"); + connector.mark_security_upgrade_as_done(); + Upgraded +} + +#[instrument(skip_all)] +pub fn connect_finalize( + _: Upgraded, + mut connector: ClientConnector, + framed: &mut Framed, + network_client: &mut impl NetworkClient, + server_name: ServerName, + server_public_key: Vec, + kerberos_config: Option, +) -> ConnectorResult +where + S: Read + Write, +{ + let mut buf = WriteBuf::new(); + + debug!("CredSSP procedure"); + + if connector.should_perform_credssp() { + perform_credssp_step( + &mut connector, + framed, + network_client, + &mut buf, + server_name, + server_public_key, + kerberos_config, + )?; + } + + debug!("Remaining of connection sequence"); + + let result = loop { + single_sequence_step(framed, &mut connector, &mut buf)?; + + if let ClientConnectorState::Connected { result } = connector.state { + break result; + } + }; + + info!("Connected with success"); + + Ok(result) +} + +fn resolve_generator( + generator: &mut CredsspProcessGenerator<'_>, + network_client: &mut impl NetworkClient, +) -> ConnectorResult { + let mut state = generator.start(); + + loop { + match state { + GeneratorState::Suspended(request) => { + let response = network_client.send(&request).map_err(|e| { + ConnectorError::new("network client send", ironrdp_connector::ConnectorErrorKind::Credssp(e)) + })?; + state = generator.resume(Ok(response)); + } + GeneratorState::Completed(client_state) => { + break client_state + .map_err(|e| ConnectorError::new("CredSSP", ironrdp_connector::ConnectorErrorKind::Credssp(e))) + } + } + } +} + +#[instrument(level = "trace", skip_all)] +fn perform_credssp_step( + connector: &mut ClientConnector, + framed: &mut Framed, + network_client: &mut impl NetworkClient, + buf: &mut WriteBuf, + server_name: ServerName, + server_public_key: Vec, + kerberos_config: Option, +) -> ConnectorResult<()> +where + S: Read + Write, +{ + assert!(connector.should_perform_credssp()); + + let selected_protocol = match connector.state { + ClientConnectorState::Credssp { selected_protocol, .. } => selected_protocol, + _ => return Err(general_err!("invalid connector state for CredSSP sequence")), + }; + + let (mut sequence, mut ts_request) = CredsspSequence::init( + connector.config.credentials.clone(), + connector.config.domain.as_deref(), + selected_protocol, + server_name, + server_public_key, + kerberos_config, + )?; + + loop { + let client_state = { + let mut generator = sequence.process_ts_request(ts_request); + resolve_generator(&mut generator, network_client)? + }; // drop generator + + buf.clear(); + let written = sequence.handle_process_result(client_state, buf)?; + + if let Some(response_len) = written.size() { + let response = &buf[..response_len]; + trace!(response_len, "Send response"); + framed + .write_all(response) + .map_err(|e| ironrdp_connector::custom_err!("write all", e))?; + } + + let Some(next_pdu_hint) = sequence.next_pdu_hint() else { + break; + }; + + debug!( + connector.state = connector.state.name(), + hint = ?next_pdu_hint, + "Wait for PDU" + ); + + let pdu = framed + .read_by_hint(next_pdu_hint) + .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; + + trace!(length = pdu.len(), "PDU received"); + + if let Some(next_request) = sequence.decode_server_message(&pdu)? { + ts_request = next_request; + } else { + break; + } + } + + connector.mark_credssp_as_done(); + + Ok(()) +} + +pub fn single_sequence_step( + framed: &mut Framed, + connector: &mut ClientConnector, + buf: &mut WriteBuf, +) -> ConnectorResult<()> +where + S: Read + Write, +{ + buf.clear(); + + let written = if let Some(next_pdu_hint) = connector.next_pdu_hint() { + debug!( + connector.state = connector.state.name(), + hint = ?next_pdu_hint, + "Wait for PDU" + ); + + let pdu = framed + .read_by_hint(next_pdu_hint) + .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; + + trace!(length = pdu.len(), "PDU received"); + + connector.step(&pdu, buf)? + } else { + connector.step_no_input(buf)? + }; + + if let Some(response_len) = written.size() { + let response = &buf[..response_len]; + trace!(response_len, "Send response"); + framed + .write_all(response) + .map_err(|e| ironrdp_connector::custom_err!("write all", e))?; + } + + Ok(()) +} diff --git a/crates/ironrdp-blocking/src/framed.rs b/crates/ironrdp-blocking/src/framed.rs new file mode 100644 index 00000000..b08a94a4 --- /dev/null +++ b/crates/ironrdp-blocking/src/framed.rs @@ -0,0 +1,136 @@ +use std::io::{self, Read, Write}; + +use bytes::{Bytes, BytesMut}; +use ironrdp_pdu::PduHint; +use tracing::debug; + +pub struct Framed { + stream: S, + buf: BytesMut, +} + +impl Framed { + pub fn new(stream: S) -> Self { + Self::new_with_leftover(stream, BytesMut::new()) + } + + pub fn new_with_leftover(stream: S, leftover: BytesMut) -> Self { + Self { stream, buf: leftover } + } + + pub fn into_inner(self) -> (S, BytesMut) { + (self.stream, self.buf) + } + + pub fn into_inner_no_leftover(self) -> S { + let (stream, leftover) = self.into_inner(); + debug_assert_eq!(leftover.len(), 0, "unexpected leftover"); + stream + } + + pub fn get_inner(&self) -> (&S, &BytesMut) { + (&self.stream, &self.buf) + } + + pub fn get_inner_mut(&mut self) -> (&mut S, &mut BytesMut) { + (&mut self.stream, &mut self.buf) + } + + pub fn peek(&self) -> &[u8] { + &self.buf + } +} + +impl Framed +where + S: Read, +{ + /// Accumulates at least `length` bytes and returns exactly `length` bytes, keeping the leftover in the internal buffer. + pub fn read_exact(&mut self, length: usize) -> io::Result { + loop { + if self.buf.len() >= length { + return Ok(self.buf.split_to(length)); + } else { + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked underflow)")] + self.buf + .reserve(length.checked_sub(self.buf.len()).expect("length > self.buf.len()")); + } + + let len = self.read()?; + + // Handle EOF + if len == 0 { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); + } + } + } + + /// Reads a standard RDP PDU frame. + pub fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, BytesMut)> { + loop { + // Try decoding and see if a frame has been received already + match ironrdp_pdu::find_size(self.peek()) { + Ok(Some(pdu_info)) => { + let frame = self.read_exact(pdu_info.length)?; + + return Ok((pdu_info.action, frame)); + } + Ok(None) => { + let len = self.read()?; + + // Handle EOF + if len == 0 { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); + } + } + Err(e) => return Err(io::Error::other(e)), + }; + } + } + + /// Reads a frame using the provided PduHint. + pub fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result { + loop { + match hint.find_size(self.peek()).map_err(io::Error::other)? { + Some((matched, length)) => { + let bytes = self.read_exact(length)?.freeze(); + if matched { + return Ok(bytes); + } else { + debug!("Received and lost an unexpected PDU"); + } + } + None => { + let len = self.read()?; + + // Handle EOF + if len == 0 { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); + } + } + }; + } + } + + /// Reads from stream and fills internal buffer, returning how many bytes were read. + fn read(&mut self) -> io::Result { + // FIXME(perf): use read_buf (https://doc.rust-lang.org/std/io/trait.Read.html#method.read_buf) + // once its stabilized. See tracking issue for RFC 2930: https://github.com/rust-lang/rust/issues/78485 + + let mut read_bytes = [0u8; 1024]; + let len = self.stream.read(&mut read_bytes)?; + self.buf.extend_from_slice(&read_bytes[..len]); + + Ok(len) + } +} + +impl Framed +where + S: Write, +{ + /// Attempts to write an entire buffer into this `Framed`’s stream. + pub fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + self.stream.write_all(buf) + } +} diff --git a/crates/ironrdp-blocking/src/lib.rs b/crates/ironrdp-blocking/src/lib.rs new file mode 100644 index 00000000..e5955ac3 --- /dev/null +++ b/crates/ironrdp-blocking/src/lib.rs @@ -0,0 +1,9 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +mod connector; +mod framed; +mod session; + +pub use self::connector::*; +pub use self::framed::*; diff --git a/crates/ironrdp-blocking/src/session.rs b/crates/ironrdp-blocking/src/session.rs new file mode 100644 index 00000000..5c31b31d --- /dev/null +++ b/crates/ironrdp-blocking/src/session.rs @@ -0,0 +1 @@ +// TODO: active session I/O helpers? I’m not yet sure we need that diff --git a/crates/ironrdp-cfg/Cargo.toml b/crates/ironrdp-cfg/Cargo.toml new file mode 100644 index 00000000..270e330f --- /dev/null +++ b/crates/ironrdp-cfg/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ironrdp-cfg" +version = "0.1.0" +readme = "README.md" +description = "IronRDP utilities for ironrdp-cfgstore" +publish = false # TODO: publish +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } # public + +[lints] +workspace = true diff --git a/crates/ironrdp-cfg/README.md b/crates/ironrdp-cfg/README.md new file mode 100644 index 00000000..6cdd08d0 --- /dev/null +++ b/crates/ironrdp-cfg/README.md @@ -0,0 +1,7 @@ +# IronRDP Configuration + +IronRDP-related utilities for ironrdp-propertyset. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-cfg/src/lib.rs b/crates/ironrdp-cfg/src/lib.rs new file mode 100644 index 00000000..e9a00061 --- /dev/null +++ b/crates/ironrdp-cfg/src/lib.rs @@ -0,0 +1,63 @@ +// QUESTION: consider auto-generating this file based on a reference file? +// https://gist.github.com/awakecoding/838c7fe2ed3a6208e3ca5d8af25363f6 + +use ironrdp_propertyset::PropertySet; + +pub trait PropertySetExt { + fn full_address(&self) -> Option<&str>; + + fn server_port(&self) -> Option; + + fn alternate_full_address(&self) -> Option<&str>; + + fn gateway_hostname(&self) -> Option<&str>; + + fn remote_application_name(&self) -> Option<&str>; + + fn remote_application_program(&self) -> Option<&str>; + + fn kdc_proxy_url(&self) -> Option<&str>; + + fn username(&self) -> Option<&str>; + + /// Target RDP server password - use for testing only + fn clear_text_password(&self) -> Option<&str>; +} + +impl PropertySetExt for PropertySet { + fn full_address(&self) -> Option<&str> { + self.get::<&str>("full address") + } + + fn server_port(&self) -> Option { + self.get::("server port") + } + + fn alternate_full_address(&self) -> Option<&str> { + self.get::<&str>("alternate full address") + } + + fn gateway_hostname(&self) -> Option<&str> { + self.get::<&str>("gatewayhostname") + } + + fn remote_application_name(&self) -> Option<&str> { + self.get::<&str>("remoteapplicationname") + } + + fn remote_application_program(&self) -> Option<&str> { + self.get::<&str>("remoteapplicationprogram") + } + + fn kdc_proxy_url(&self) -> Option<&str> { + self.get::<&str>("kdcproxyurl") + } + + fn username(&self) -> Option<&str> { + self.get::<&str>("username") + } + + fn clear_text_password(&self) -> Option<&str> { + self.get::<&str>("ClearTextPassword") + } +} diff --git a/crates/ironrdp-client-glutin/Cargo.toml b/crates/ironrdp-client-glutin/Cargo.toml new file mode 100644 index 00000000..062600cf --- /dev/null +++ b/crates/ironrdp-client-glutin/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "ironrdp-client-glutin" +version = "0.1.0" +readme = "README.md" +description = "GPU-accelerated RDP client using glutin" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[features] +default = ["rustls"] +rustls = ["ironrdp-tls/rustls"] +native-tls = ["ironrdp-tls/native-tls"] + +[dependencies] + +# Protocols +ironrdp.workspace = true +ironrdp-tls.workspace = true +sspi = { workspace = true, features = ["network_client"] } + +# CLI +clap = { version = "4.2", features = ["derive", "cargo"] } +exitcode = "1.1" + +# logging +tracing.workspace = true +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# async, futures +tokio = { version = "1", features = ["full"]} +tokio-util = { version = "0.7", features = ["compat"] } +futures-util = "0.3" + +# Utils +chrono = "0.4" +anyhow = "1.0" + +# GUI +glutin = "0.29" +ironrdp-glutin-renderer = { path = "../glutin-renderer"} + +[lints] +workspace = true + diff --git a/crates/ironrdp-client-glutin/README.md b/crates/ironrdp-client-glutin/README.md new file mode 100644 index 00000000..a5470545 --- /dev/null +++ b/crates/ironrdp-client-glutin/README.md @@ -0,0 +1,13 @@ +# GUI client + +1. An experimental GUI based of glutin and glow library. +2. Sample command to run the ui client: + ``` + cargo run --bin ironrdp-gui-client -- -u SimpleUsername -p SimplePassword! --avc444 --thin-client --small-cache --capabilities 0xf 192.168.1.100:3389 + ``` +3. If the GUI has artifacts it can be dumped to a file using the gfx_dump_file parameter. Later the ironrdp-replay-client binary can be used to debug and fix any issues + in the renderer. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-client-glutin/src/config.rs b/crates/ironrdp-client-glutin/src/config.rs new file mode 100644 index 00000000..64e58723 --- /dev/null +++ b/crates/ironrdp-client-glutin/src/config.rs @@ -0,0 +1,186 @@ +use std::num::ParseIntError; +use std::path::PathBuf; + +use clap::clap_derive::ValueEnum; +use clap::{crate_name, Parser}; +use ironrdp::session::{GraphicsConfig, InputConfig}; +use sspi::AuthIdentity; + +const DEFAULT_WIDTH: u16 = 1920; +const DEFAULT_HEIGHT: u16 = 1080; +const GLOBAL_CHANNEL_NAME: &str = "GLOBAL"; +const USER_CHANNEL_NAME: &str = "USER"; + +pub struct Config { + pub log_file: String, + pub addr: String, + pub input: InputConfig, + pub gfx_dump_file: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum SecurityProtocol { + Ssl, + Hybrid, + HybridEx, +} + +impl SecurityProtocol { + fn parse(security_protocol: SecurityProtocol) -> ironrdp::pdu::SecurityProtocol { + match security_protocol { + SecurityProtocol::Ssl => ironrdp::pdu::SecurityProtocol::SSL, + SecurityProtocol::Hybrid => ironrdp::pdu::SecurityProtocol::HYBRID, + SecurityProtocol::HybridEx => ironrdp::pdu::SecurityProtocol::HYBRID_EX, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum KeyboardType { + IbmPcXt, + OlivettiIco, + IbmPcAt, + IbmEnhanced, + Nokia1050, + Nokia9140, + Japanese, +} + +impl KeyboardType { + fn parse(keyboard_type: KeyboardType) -> ironrdp::pdu::gcc::KeyboardType { + match keyboard_type { + KeyboardType::IbmEnhanced => ironrdp::pdu::gcc::KeyboardType::IbmEnhanced, + KeyboardType::IbmPcAt => ironrdp::pdu::gcc::KeyboardType::IbmPcAt, + KeyboardType::IbmPcXt => ironrdp::pdu::gcc::KeyboardType::IbmPcXt, + KeyboardType::OlivettiIco => ironrdp::pdu::gcc::KeyboardType::OlivettiIco, + KeyboardType::Nokia1050 => ironrdp::pdu::gcc::KeyboardType::Nokia1050, + KeyboardType::Nokia9140 => ironrdp::pdu::gcc::KeyboardType::Nokia9140, + KeyboardType::Japanese => ironrdp::pdu::gcc::KeyboardType::Japanese, + } + } +} + +fn parse_hex(input: &str) -> Result { + if input.starts_with("0x") { + u32::from_str_radix(input.get(2..).unwrap_or(""), 16) + } else { + input.parse::() + } +} +/// Devolutions IronRDP client +#[derive(Parser, Debug)] +#[clap(author = "Devolutions", about = "Devolutions-IronRDP client")] +#[clap(version, long_about = None)] +struct Args { + /// A file with IronRDP client logs + #[clap(short, long, value_parser, default_value_t = format!("{}.log", crate_name!()))] + log_file: String, + + /// An address on which the client will connect. + addr: String, + + /// A target RDP server user name + #[clap(short, long, value_parser)] + username: String, + + /// An optional target RDP server domain name + #[clap(short, long, value_parser)] + domain: Option, + + /// A target RDP server user password + #[clap(short, long, value_parser)] + password: String, + + /// Specify the security protocols to use + #[clap(long, value_enum, value_parser, default_value_t = SecurityProtocol::HybridEx)] + security_protocol: SecurityProtocol, + + /// The keyboard type + #[clap(long, value_enum, value_parser, default_value_t = KeyboardType::IbmEnhanced)] + keyboard_type: KeyboardType, + + /// The keyboard subtype (an original equipment manufacturer-dependent value) + #[clap(long, value_parser, default_value_t = 0)] + keyboard_subtype: u32, + + /// The number of function keys on the keyboard + #[clap(long, value_parser, default_value_t = 12)] + keyboard_functional_keys_count: u32, + + /// The input method editor (IME) file name associated with the active input locale + #[clap(long, value_parser, default_value_t = String::from(""))] + ime_file_name: String, + + /// Contains a value that uniquely identifies the client + #[clap(long, value_parser, default_value_t = String::from(""))] + dig_product_id: String, + + /// Enable AVC444 + #[clap(long, group = "avc")] + avc444: bool, + + /// Enable H264 + #[clap(long, group = "avc")] + h264: bool, + + /// Enable thin client + #[clap(long)] + thin_client: bool, + + /// Enable small cache + #[clap(long)] + small_cache: bool, + + /// Enabled capability versions. Each bit represents enabling a capability version + /// starting from V8 to V10_7 + #[clap(long, value_parser = parse_hex, default_value_t = 0)] + capabilities: u32, + + /// Enables dumping the gfx stream to a file location + #[clap(long, value_parser)] + gfx_dump_file: Option, +} + +impl Config { + pub fn parse_args() -> Self { + let args = Args::parse(); + + let graphics_config = if args.avc444 || args.h264 { + Some(GraphicsConfig { + avc444: args.avc444, + h264: args.h264, + thin_client: args.thin_client, + small_cache: args.small_cache, + capabilities: args.capabilities, + }) + } else { + None + }; + + let input = InputConfig { + credentials: AuthIdentity { + username: args.username, + password: args.password.into(), + domain: args.domain, + }, + security_protocol: SecurityProtocol::parse(args.security_protocol), + keyboard_type: KeyboardType::parse(args.keyboard_type), + keyboard_subtype: args.keyboard_subtype, + keyboard_functional_keys_count: args.keyboard_functional_keys_count, + ime_file_name: args.ime_file_name, + dig_product_id: args.dig_product_id, + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + global_channel_name: GLOBAL_CHANNEL_NAME.to_string(), + user_channel_name: USER_CHANNEL_NAME.to_string(), + graphics_config, + }; + + Self { + log_file: args.log_file, + addr: args.addr, + input, + gfx_dump_file: args.gfx_dump_file, + } + } +} diff --git a/crates/ironrdp-client-glutin/src/gui.rs b/crates/ironrdp-client-glutin/src/gui.rs new file mode 100644 index 00000000..45e5d424 --- /dev/null +++ b/crates/ironrdp-client-glutin/src/gui.rs @@ -0,0 +1,147 @@ +use std::fmt::Debug; +use std::path::PathBuf; +use std::sync::mpsc::{Receiver, SyncSender}; +use std::sync::{self, Arc}; + +use glutin::dpi::PhysicalPosition; +use glutin::event::{Event, WindowEvent}; +use glutin::event_loop::ControlFlow; +use ironrdp::pdu::dvc::gfx::ServerPdu; +use ironrdp::session::{ErasedWriter, GfxHandler}; +use ironrdp_glutin_renderer::renderer::Renderer; +use tokio::sync::Mutex; + +use self::input::{handle_input_events, translate_input_event}; +use crate::RdpError; + +mod input; + +#[derive(Debug, Clone)] +pub struct MessagePassingGfxHandler { + channel: SyncSender, +} + +impl MessagePassingGfxHandler { + pub fn new(channel: SyncSender) -> Self { + Self { channel } + } +} + +impl GfxHandler for MessagePassingGfxHandler { + fn on_message(&self, message: ServerPdu) -> Result, RdpError> { + self.channel.send(message).map_err(|e| RdpError::Send(e.to_string()))?; + Ok(None) + } +} + +pub struct UiContext { + window: glutin::ContextWrapper, + event_loop: glutin::event_loop::EventLoop, +} + +impl UiContext { + fn create_ui_context( + width: i32, + height: i32, + ) -> ( + glutin::ContextWrapper, + glutin::event_loop::EventLoop, + ) { + let event_loop = glutin::event_loop::EventLoopBuilder::with_user_event().build(); + let window_builder = glutin::window::WindowBuilder::new() + .with_title("IronRDP Client") + .with_resizable(false) + .with_inner_size(glutin::dpi::PhysicalSize::new(width, height)); + let window = glutin::ContextBuilder::new() + .with_vsync(true) + .build_windowed(window_builder, &event_loop) + .unwrap(); + (window, event_loop) + } + + pub fn new(width: u16, height: u16) -> Self { + let (window, event_loop) = UiContext::create_ui_context(width as i32, height as i32); + UiContext { window, event_loop } + } +} + +#[derive(Debug)] +pub enum UserEvent {} + +/// Launches the GUI. Because of the way UI programming works the event loop has to be run from main thread +pub fn launch_gui( + context: UiContext, + gfx_dump_file: Option, + graphic_receiver: Receiver, + stream: Arc>, +) -> Result<(), RdpError> { + let (sender, receiver) = sync::mpsc::channel(); + + tokio::spawn(async move { handle_input_events(receiver, stream).await }); + + let renderer = Renderer::new(context.window, graphic_receiver, gfx_dump_file); + // We handle events differently between targets + + let mut last_position: Option> = None; + context.event_loop.run(move |main_event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match &main_event { + Event::LoopDestroyed => {} + Event::RedrawRequested(_) => { + let res = renderer.repaint(); + if res.is_err() { + error!("Repaint send error: {:?}", res); + } + } + Event::WindowEvent { ref event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::Resized(..) => { + // let width = new_size.width; + // let height = new_size.height; + // let scale_factor = window.window().scale_factor(); + // info!("Scale factor: {} Window size: {:?}x {:?}", scale_factor, width, height); + // let layout_pdu = display::ClientPdu::DisplayControlMonitorLayout(MonitorLayoutPdu { + // monitors: vec![Monitor { + // left: 0, + // top: 0, + // width: width, + // height: height, + // flags: MonitorFlags::PRIMARY, + // physical_width: 0, + // physical_height: 0, + // orientation: Orientation::Landscape, + // desktop_scale_factor: 0, + // device_scale_factor: 0, + // }], + // }); + // let mut data_buffer = Vec::new(); + // layout_pdu.to_buffer(&mut data_buffer)?; + // if let (Some(x224_processor), Some(stream)) = (x224_processor.as_ref(), stream.as_mut()) { + // let mut x224_processor = x224_processor.lock()?; + // // Ignorable error in case of display channel is not connected + // let result = + // x224_processor.send_dynamic(&mut *stream, x224::RDP8_DISPLAY_PIPELINE_NAME, data_buffer); + // if result.is_err() { + // error!("Monitor layout {:?}", result); + // } else { + // error!("Monitor layout success"); + // } + // } + } + WindowEvent::KeyboardInput { .. } + | WindowEvent::MouseInput { .. } + | WindowEvent::CursorMoved { .. } => { + if let Some(event) = translate_input_event(main_event, &mut last_position) { + let result = sender.send(event); + if result.is_err() { + error!("Send of event failed: {:?}", result); + } + } + } + _ => {} + }, + _ => (), + } + }) +} diff --git a/crates/ironrdp-client-glutin/src/gui/input.rs b/crates/ironrdp-client-glutin/src/gui/input.rs new file mode 100644 index 00000000..83391b21 --- /dev/null +++ b/crates/ironrdp-client-glutin/src/gui/input.rs @@ -0,0 +1,92 @@ +use std::sync::mpsc::Receiver; +use std::sync::Arc; + +use futures_util::AsyncWriteExt; +use glutin::dpi::PhysicalPosition; +use glutin::event::{ElementState, Event, WindowEvent}; +use ironrdp::pdu::input::fast_path::{FastPathInput, FastPathInputEvent, KeyboardFlags}; +use ironrdp::pdu::input::mouse::PointerFlags; +use ironrdp::pdu::input::MousePdu; +use ironrdp::session::ErasedWriter; +use tokio::sync::Mutex; + +use super::UserEvent; + +pub async fn handle_input_events(receiver: Receiver, event_stream: Arc>) { + loop { + let mut fastpath_events = Vec::new(); + let event = receiver.recv().unwrap(); + fastpath_events.push(event); + while let Ok(event) = receiver.try_recv() { + fastpath_events.push(event); + } + let mut data: Vec = Vec::new(); + let input_pdu = FastPathInput(fastpath_events); + input_pdu.to_buffer(&mut data).unwrap(); + let mut event_stream = event_stream.lock().await; + let _result = event_stream.write_all(data.as_slice()).await; + let _result = event_stream.flush().await; + } +} + +pub fn translate_input_event( + event: Event, + last_position: &mut Option>, +) -> Option { + match event { + Event::WindowEvent { ref event, .. } => match event { + WindowEvent::KeyboardInput { + device_id: _, + input, + is_synthetic: _, + } => { + let scan_code = input.scancode & 0xff; + + let flags = match input.state { + ElementState::Pressed => KeyboardFlags::empty(), + ElementState::Released => KeyboardFlags::RELEASE, + }; + Some(FastPathInputEvent::KeyboardEvent(flags, scan_code as u8)) + } + WindowEvent::MouseInput { state, button, .. } => { + if let Some(position) = last_position.as_ref() { + let button = match button { + glutin::event::MouseButton::Left => PointerFlags::LEFT_BUTTON, + glutin::event::MouseButton::Right => PointerFlags::RIGHT_BUTTON, + glutin::event::MouseButton::Middle => PointerFlags::MIDDLE_BUTTON_OR_WHEEL, + glutin::event::MouseButton::Other(_) => PointerFlags::empty(), + }; + let button_events = button + | match state { + ElementState::Pressed => PointerFlags::DOWN, + ElementState::Released => PointerFlags::empty(), + }; + let pdu = MousePdu { + x_position: position.x as u16, + y_position: position.y as u16, + flags: button_events, + number_of_wheel_rotation_units: 0, + }; + + Some(FastPathInputEvent::MouseEvent(pdu)) + } else { + None + } + } + WindowEvent::CursorMoved { position, .. } => { + *last_position = Some(*position); + + let pdu = MousePdu { + x_position: position.x as u16, + y_position: position.y as u16, + flags: PointerFlags::MOVE, + number_of_wheel_rotation_units: 0, + }; + + Some(FastPathInputEvent::MouseEvent(pdu)) + } + _ => None, + }, + _ => None, + } +} diff --git a/crates/ironrdp-client-glutin/src/main.rs b/crates/ironrdp-client-glutin/src/main.rs new file mode 100644 index 00000000..c65aa9a5 --- /dev/null +++ b/crates/ironrdp-client-glutin/src/main.rs @@ -0,0 +1,258 @@ +mod config; + +use std::sync::mpsc::sync_channel; +use std::sync::Arc; +use std::{io, process}; + +use anyhow::Context as _; +use futures_util::io::AsyncWriteExt as _; +use gui::MessagePassingGfxHandler; +use ironrdp::graphics::image_processing::PixelFormat; +use ironrdp::pdu::dvc::gfx::ServerPdu; +use ironrdp::session::connection_sequence::{process_connection_sequence, UpgradedStream}; +use ironrdp::session::image::DecodedImage; +use ironrdp::session::{ActiveStageOutput, ActiveStageProcessor, ErasedWriter, RdpError}; +use sspi::network_client::reqwest_network_client::RequestClientFactory; +use tokio::io::AsyncWriteExt as _; +use tokio::net::TcpStream; +use tokio::sync::Mutex; +use tokio_util::compat::TokioAsyncReadCompatExt as _; +use x509_parser::prelude::{FromDer as _, X509Certificate}; + +use crate::config::Config; + +#[cfg(feature = "rustls")] +type TlsStream = tokio_util::compat::Compat>; + +#[cfg(all(feature = "native-tls", not(feature = "rustls")))] +type TlsStream = tokio_util::compat::Compat>; + +mod gui; + +#[cfg(feature = "rustls")] +mod danger { + use std::time::SystemTime; + + use tokio_rustls::rustls::client::ServerCertVerified; + use tokio_rustls::rustls::{Certificate, Error, ServerName}; + + pub struct NoCertificateVerification; + + impl tokio_rustls::rustls::client::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: SystemTime, + ) -> Result { + Ok(tokio_rustls::rustls::client::ServerCertVerified::assertion()) + } + } +} + +#[tokio::main] +async fn main() { + let config = Config::parse_args(); + setup_logging(config.log_file.as_str()).expect("failed to initialize logging"); + + let exit_code = match run(config).await { + Ok(_) => { + println!("RDP successfully finished"); + exitcode::OK + } + Err(RdpError::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => { + error!("{}", e); + println!("The server has terminated the RDP session"); + exitcode::NOHOST + } + Err(ref e) => { + error!("{}", e); + println!("RDP failed because of {e}"); + + match e { + RdpError::Io(_) => exitcode::IOERR, + RdpError::Connection(_) => exitcode::NOHOST, + _ => exitcode::PROTOCOL, + } + } + }; + + std::process::exit(exit_code); +} + +fn setup_logging(log_file: &str) -> anyhow::Result<()> { + use std::fs::OpenOptions; + + use tracing::metadata::LevelFilter; + use tracing_subscriber::prelude::*; + use tracing_subscriber::EnvFilter; + + let file = OpenOptions::new() + .create(true) + .append(true) + .open(log_file) + .with_context(|| format!("Couldn’t open {log_file}"))?; + + let fmt_layer = tracing_subscriber::fmt::layer() + .compact() + .with_ansi(false) + .with_writer(file); + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .with_env_var("IRONRDP_LOG_LEVEL") + .from_env_lossy(); + + let reg = tracing_subscriber::registry().with(fmt_layer).with(env_filter); + + tracing::subscriber::set_global_default(reg).context("Failed to set tracing global subscriber")?; + + Ok(()) +} + +async fn run(config: Config) -> Result<(), RdpError> { + let addr = ironrdp::session::connection_sequence::Address::lookup_addr(&config.addr)?; + + let stream = TcpStream::connect(addr.sock).await.map_err(RdpError::Connection)?; + + let (connection_sequence_result, reader, writer) = process_connection_sequence( + stream.compat(), + &addr, + &config.input, + establish_tls, + Box::new(RequestClientFactory), + ) + .await?; + + let writer = Arc::new(Mutex::new(writer)); + let image = DecodedImage::new( + PixelFormat::RgbA32, + connection_sequence_result.desktop_size.width, + connection_sequence_result.desktop_size.height, + ); + + launch_client(config, connection_sequence_result, image, reader, writer).await +} + +async fn launch_client( + config: Config, + connection_sequence_result: ironrdp::session::connection_sequence::ConnectionSequenceResult, + image: DecodedImage, + reader: ironrdp::session::FramedReader, + writer: Arc>, +) -> Result<(), RdpError> { + let (sender, receiver) = sync_channel::(1); + let handler = MessagePassingGfxHandler::new(sender); + let active_stage = ActiveStageProcessor::new( + config.input.clone(), + Some(Box::new(handler)), + connection_sequence_result, + ); + let gui = gui::UiContext::new(config.input.width, config.input.height); + + let active_stage_writer = writer.clone(); + let active_stage_handle = tokio::spawn(async move { + match process_active_stage(reader, active_stage, image, active_stage_writer).await { + Ok(()) => Ok(()), + Err(error) => { + error!(?error, "Active stage failed"); + process::exit(-1); + } + } + }); + gui::launch_gui(gui, config.gfx_dump_file, receiver, writer.clone())?; + active_stage_handle.await.map_err(|e| RdpError::Io(e.into()))? +} + +async fn process_active_stage( + mut reader: ironrdp::session::FramedReader, + mut active_stage: ActiveStageProcessor, + mut image: DecodedImage, + writer: Arc>, +) -> Result<(), RdpError> { + 'outer: loop { + let frame = reader.read_frame().await?.ok_or(RdpError::AccessDenied)?.freeze(); + let outputs = active_stage.process(&mut image, frame)?; + for out in outputs { + match out { + ActiveStageOutput::ResponseFrame(frame) => { + let mut writer = writer.lock().await; + writer.write_all(&frame).await? + } + ActiveStageOutput::GraphicsUpdate(_region) => {} + ActiveStageOutput::Terminate => break 'outer, + } + } + } + Ok(()) +} + +// TODO: this can be refactored into a separate `ironrdp-tls` crate (all native clients will do the same TLS dance) +async fn establish_tls(stream: tokio_util::compat::Compat) -> Result, RdpError> { + let stream = stream.into_inner(); + + #[cfg(all(feature = "native-tls", not(feature = "rustls")))] + let mut tls_stream = { + let connector = async_native_tls::TlsConnector::new() + .danger_accept_invalid_certs(true) + .use_sni(false); + + // domain is an empty string because client accepts IP address in the cli + match connector.connect("", stream).await { + Ok(tls) => tls, + Err(err) => return Err(RdpError::TlsHandshake(err)), + } + }; + + #[cfg(feature = "rustls")] + let mut tls_stream = { + let mut client_config = tokio_rustls::rustls::client::ClientConfig::builder() + .with_safe_defaults() + .with_custom_certificate_verifier(std::sync::Arc::new(danger::NoCertificateVerification)) + .with_no_client_auth(); + // This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret) + client_config.key_log = std::sync::Arc::new(tokio_rustls::rustls::KeyLogFile::new()); + let rc_config = std::sync::Arc::new(client_config); + let example_com = "stub_string".try_into().unwrap(); + let connector = tokio_rustls::TlsConnector::from(rc_config); + connector.connect(example_com, stream).await? + }; + + tls_stream.flush().await?; + + #[cfg(all(feature = "native-tls", not(feature = "rustls")))] + let server_public_key = { + let cert = tls_stream + .peer_certificate() + .map_err(RdpError::TlsConnector)? + .ok_or(RdpError::MissingPeerCertificate)?; + get_tls_peer_pubkey(cert.to_der().map_err(RdpError::DerEncode)?)? + }; + + #[cfg(feature = "rustls")] + let server_public_key = { + let cert = tls_stream + .get_ref() + .1 + .peer_certificates() + .ok_or(RdpError::MissingPeerCertificate)?[0] + .as_ref(); + get_tls_peer_pubkey(cert.to_vec())? + }; + + Ok(UpgradedStream { + stream: tls_stream.compat(), + server_public_key, + }) +} + +pub fn get_tls_peer_pubkey(cert: Vec) -> io::Result> { + let res = X509Certificate::from_der(&cert[..]) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid der certificate."))?; + let public_key = res.1.tbs_certificate.subject_pki.subject_public_key; + + Ok(public_key.data.to_vec()) +} diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml new file mode 100644 index 00000000..5a684444 --- /dev/null +++ b/crates/ironrdp-client/Cargo.toml @@ -0,0 +1,94 @@ +[package] +name = "ironrdp-client" +version = "0.1.0" +readme = "README.md" +description = "Portable RDP client without GPU acceleration" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true +default-run = "ironrdp-client" + +# Not publishing for now. +publish = false + +[lib] +doctest = false +test = false + +[[bin]] +name = "ironrdp-client" +test = false + +[features] +default = ["rustls"] +rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots", "ironrdp-mstsgu/rustls"] +native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls", "ironrdp-mstsgu/native-tls"] +qoi = ["ironrdp/qoi"] +qoiz = ["ironrdp/qoiz"] + +[dependencies] +# Protocols +ironrdp = { path = "../ironrdp", version = "0.14", features = [ + "session", + "input", + "graphics", + "dvc", + "svc", + "rdpdr", + "rdpsnd", + "cliprdr", + "displaycontrol", + "connector", +] } +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } +ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.5" } +ironrdp-rdpsnd-native = { path = "../ironrdp-rdpsnd-native", version = "0.4" } +ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" } +ironrdp-mstsgu = { path = "../ironrdp-mstsgu" } +ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.8", features = ["reqwest"] } +ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" +ironrdp-dvc-pipe-proxy.path = "../ironrdp-dvc-pipe-proxy" +ironrdp-propertyset.path = "../ironrdp-propertyset" +ironrdp-rdpfile.path = "../ironrdp-rdpfile" +ironrdp-cfg.path = "../ironrdp-cfg" + +# Windowing and rendering +winit = { version = "0.30", features = ["rwh_06"] } +softbuffer = "0.4" + +# CLI +clap = { version = "4.5", features = ["derive", "cargo"] } +proc-exit = "2" +inquire = "0.9" + +# Logging +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Async, futures +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7" } +tokio-tungstenite = "0.28" +transport = { git = "https://github.com/Devolutions/devolutions-gateway", rev = "06e91dfe82751a6502eaf74b6a99663f06f0236d" } +futures-util = { version = "0.3", features = ["sink"] } + +# Utils +whoami = "1.6" +anyhow = "1" +smallvec = "1.15" +tap = "1" +semver = "1" +raw-window-handle = "0.6" +uuid = { version = "1.19" } +x509-cert = { version = "0.2", default-features = false, features = ["std"] } +url = "2" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62", features = ["Win32_Foundation"] } + +[lints] +workspace = true diff --git a/crates/ironrdp-client/README.md b/crates/ironrdp-client/README.md new file mode 100644 index 00000000..95a9f90f --- /dev/null +++ b/crates/ironrdp-client/README.md @@ -0,0 +1,48 @@ +# IronRDP client + +Portable RDP client without GPU acceleration. + +This is a a full-fledged RDP client based on IronRDP crates suite, and implemented using +non-blocking, asynchronous I/O. Portability is achieved by using softbuffer for rendering +and winit for windowing. + +## Sample usage + +```shell +ironrdp-client --username --password +``` + +## Configuring log filter directives + +The `IRONRDP_LOG` environment variable is used to set the log filter directives. + +```shell +IRONRDP_LOG="info,ironrdp_connector=trace" ironrdp-client --username --password +``` + +See [`tracing-subscriber`’s documentation][tracing-doc] for more details. + +[tracing-doc]: https://docs.rs/tracing-subscriber/0.3.17/tracing_subscriber/filter/struct.EnvFilter.html#directives + +## Support for `SSLKEYLOGFILE` + +This client supports reading the `SSLKEYLOGFILE` environment variable. +When set, the TLS encryption secrets for the session will be dumped to the file specified +by the environment variable. +This file can be read by Wireshark so that in can decrypt the packets. + +### Example + +```shell +SSLKEYLOGFILE=/tmp/tls-secrets ironrdp-client --username --password +``` + +### Usage in Wireshark + +See this [awakecoding's repository][awakecoding-repository] explaining how to use the file in wireshark. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP +[awakecoding-repository]: https://github.com/awakecoding/wireshark-rdp#sslkeylogfile + diff --git a/crates/ironrdp-client/src/app.rs b/crates/ironrdp-client/src/app.rs new file mode 100644 index 00000000..bcce8d4f --- /dev/null +++ b/crates/ironrdp-client/src/app.rs @@ -0,0 +1,414 @@ +#![allow(clippy::print_stderr, clippy::print_stdout)] // allowed in this module only + +use core::num::NonZeroU32; +use core::time::Duration; +use std::sync::Arc; +use std::time::Instant; + +use anyhow::Context as _; +use raw_window_handle::{DisplayHandle, HasDisplayHandle as _}; +use tokio::sync::mpsc; +use tracing::{debug, error, trace, warn}; +use winit::application::ApplicationHandler; +use winit::dpi::{LogicalPosition, PhysicalSize}; +use winit::event::{self, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::platform::scancode::PhysicalKeyExtScancode as _; +use winit::window::{CursorIcon, CustomCursor, Window, WindowAttributes}; + +use crate::rdp::{RdpInputEvent, RdpOutputEvent}; + +type WindowSurface = (Arc, softbuffer::Surface, Arc>); + +pub struct App { + input_event_sender: mpsc::UnboundedSender, + context: softbuffer::Context>, + window: Option, + buffer: Vec, + buffer_size: (u16, u16), + input_database: ironrdp::input::Database, + last_size: Option>, + resize_timeout: Option, +} + +impl App { + pub fn new( + event_loop: &EventLoop, + input_event_sender: &mpsc::UnboundedSender, + ) -> anyhow::Result { + // SAFETY: We drop the softbuffer context right before the event loop is stopped, thus making this safe. + // FIXME: This is not a sufficient proof and the API is actually unsound as-is. + let display_handle = unsafe { + core::mem::transmute::, DisplayHandle<'static>>( + event_loop.display_handle().context("get display handle")?, + ) + }; + let context = softbuffer::Context::new(display_handle) + .map_err(|e| anyhow::anyhow!("unable to initialize softbuffer context: {e}"))?; + + let input_database = ironrdp::input::Database::new(); + Ok(Self { + input_event_sender: input_event_sender.clone(), + context, + window: None, + buffer: Vec::new(), + buffer_size: (0, 0), + input_database, + last_size: None, + resize_timeout: None, + }) + } + + fn send_resize_event(&mut self) { + let Some(size) = self.last_size.take() else { + return; + }; + let Some((window, _)) = self.window.as_mut() else { + return; + }; + #[expect(clippy::as_conversions, reason = "casting f64 to u32")] + let scale_factor = (window.scale_factor() * 100.0) as u32; + + let width = u16::try_from(size.width).expect("reasonable width"); + let height = u16::try_from(size.height).expect("reasonable height"); + + let _ = self.input_event_sender.send(RdpInputEvent::Resize { + width, + height, + scale_factor, + // TODO: it should be possible to get the physical size here, however winit doesn't make it straightforward. + // FreeRDP does it based on DPI reading grabbed via [`SDL_GetDisplayDPI`](https://wiki.libsdl.org/SDL2/SDL_GetDisplayDPI): + // https://github.com/FreeRDP/FreeRDP/blob/ba8cf8cf2158018fb7abbedb51ab245f369be813/client/SDL/sdl_monitor.cpp#L250-L262 + // See also: https://github.com/rust-windowing/winit/issues/826 + physical_size: None, + }); + } + + fn draw(&mut self) { + if self.buffer.is_empty() { + return; + } + let Some((_, surface)) = self.window.as_mut() else { + return; + }; + let mut sb_buffer = surface.buffer_mut().expect("surface buffer"); + sb_buffer.copy_from_slice(self.buffer.as_slice()); + sb_buffer.present().expect("buffer present"); + } +} + +impl ApplicationHandler for App { + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if let Some(timeout) = self.resize_timeout { + if let Some(timeout) = timeout.checked_duration_since(Instant::now()) { + event_loop.set_control_flow(ControlFlow::wait_duration(timeout)); + } else { + self.send_resize_event(); + self.resize_timeout = None; + event_loop.set_control_flow(ControlFlow::Wait); + } + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = WindowAttributes::default().with_title("IronRDP"); + match event_loop.create_window(window_attributes) { + Ok(window) => { + let window = Arc::new(window); + let surface = softbuffer::Surface::new(&self.context, Arc::clone(&window)).expect("surface"); + self.window = Some((window, surface)); + } + Err(error) => { + error!(%error, "Failed to create window"); + event_loop.exit(); + } + } + } + + fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: winit::window::WindowId, event: WindowEvent) { + let Some((window, _)) = self.window.as_mut() else { + return; + }; + if window_id != window.id() { + return; + } + + match event { + WindowEvent::Resized(size) => { + self.last_size = Some(size); + self.resize_timeout = Some(Instant::now() + Duration::from_secs(1)); + } + WindowEvent::CloseRequested => { + if self.input_event_sender.send(RdpInputEvent::Close).is_err() { + error!("Failed to send graceful shutdown event, closing the window"); + event_loop.exit(); + } + } + WindowEvent::DroppedFile(_) => { + // TODO(#110): File upload + } + // WindowEvent::ReceivedCharacter(_) => { + // Sadly, we can't use this winit event to send RDP unicode events because + // of the several reasons: + // 1. `ReceivedCharacter` event doesn't provide a way to distinguish between + // key press and key release, therefore the only way to use it is to send + // a key press + release events sequentially, which will not allow to + // handle long press and key repeat events. + // 2. This event do not fire for non-printable keys (e.g. Control, Alt, etc.) + // 3. This event fies BEFORE `KeyboardInput` event, so we can't make a + // reasonable workaround for `1` and `2` by collecting physical key press + // information first via `KeyboardInput` before processing `ReceivedCharacter`. + // + // However, all of these issues can be solved by updating `winit` to the + // newer version. + // + // TODO(#376): Update winit + // TODO(#376): Implement unicode input in native client + // } + WindowEvent::KeyboardInput { event, .. } => { + if let Some(scancode) = event.physical_key.to_scancode() { + let scancode = match u16::try_from(scancode) { + Ok(scancode) => scancode, + Err(_) => { + warn!("Unsupported scancode: `{scancode:#X}`; ignored"); + return; + } + }; + let scancode = ironrdp::input::Scancode::from_u16(scancode); + + let operation = match event.state { + event::ElementState::Pressed => ironrdp::input::Operation::KeyPressed(scancode), + event::ElementState::Released => ironrdp::input::Operation::KeyReleased(scancode), + }; + + let input_events = self.input_database.apply(core::iter::once(operation)); + + send_fast_path_events(&self.input_event_sender, input_events); + } + } + WindowEvent::ModifiersChanged(modifiers) => { + const SHIFT_LEFT: ironrdp::input::Scancode = ironrdp::input::Scancode::from_u8(false, 0x2A); + const CONTROL_LEFT: ironrdp::input::Scancode = ironrdp::input::Scancode::from_u8(false, 0x1D); + const ALT_LEFT: ironrdp::input::Scancode = ironrdp::input::Scancode::from_u8(false, 0x38); + const LOGO_LEFT: ironrdp::input::Scancode = ironrdp::input::Scancode::from_u8(true, 0x5B); + + let mut operations = smallvec::SmallVec::<[ironrdp::input::Operation; 4]>::new(); + + let mut add_operation = |pressed: bool, scancode: ironrdp::input::Scancode| { + let operation = if pressed { + ironrdp::input::Operation::KeyPressed(scancode) + } else { + ironrdp::input::Operation::KeyReleased(scancode) + }; + operations.push(operation); + }; + + // NOTE: https://docs.rs/winit/0.30.12/src/winit/keyboard.rs.html#1737-1744 + // + // We can’t use state.lshift_state(), state.lcontrol_state(), etc, because on some platforms such as + // Linux, the modifiers change is hidden. + // + // > The exact modifier key is not used to represent modifiers state in the + // > first place due to a fact that modifiers state could be changed without any + // > key being pressed and on some platforms like Wayland/X11 which key resulted + // > in modifiers change is hidden, also, not that it really matters. + add_operation(modifiers.state().shift_key(), SHIFT_LEFT); + add_operation(modifiers.state().control_key(), CONTROL_LEFT); + add_operation(modifiers.state().alt_key(), ALT_LEFT); + add_operation(modifiers.state().super_key(), LOGO_LEFT); + + let input_events = self.input_database.apply(operations); + + send_fast_path_events(&self.input_event_sender, input_events); + } + WindowEvent::CursorMoved { position, .. } => { + let win_size = window.inner_size(); + #[expect(clippy::as_conversions, reason = "casting f64 to u16")] + let x = (position.x / f64::from(win_size.width) * f64::from(self.buffer_size.0)) as u16; + #[expect(clippy::as_conversions, reason = "casting f64 to u16")] + let y = (position.y / f64::from(win_size.height) * f64::from(self.buffer_size.1)) as u16; + let operation = ironrdp::input::Operation::MouseMove(ironrdp::input::MousePosition { x, y }); + + let input_events = self.input_database.apply(core::iter::once(operation)); + + send_fast_path_events(&self.input_event_sender, input_events); + } + WindowEvent::MouseWheel { delta, .. } => { + let mut operations = smallvec::SmallVec::<[ironrdp::input::Operation; 2]>::new(); + + match delta { + event::MouseScrollDelta::LineDelta(delta_x, delta_y) => { + if delta_x.abs() > 0.001 { + operations.push(ironrdp::input::Operation::WheelRotations( + ironrdp::input::WheelRotations { + is_vertical: false, + #[expect(clippy::as_conversions, reason = "casting f32 to i16")] + rotation_units: (delta_x * 100.) as i16, + }, + )); + } + + if delta_y.abs() > 0.001 { + operations.push(ironrdp::input::Operation::WheelRotations( + ironrdp::input::WheelRotations { + is_vertical: true, + #[expect(clippy::as_conversions, reason = "casting f32 to i16")] + rotation_units: (delta_y * 100.) as i16, + }, + )); + } + } + event::MouseScrollDelta::PixelDelta(delta) => { + if delta.x.abs() > 0.001 { + operations.push(ironrdp::input::Operation::WheelRotations( + ironrdp::input::WheelRotations { + is_vertical: false, + #[expect(clippy::as_conversions, reason = "casting f64 to i16")] + rotation_units: delta.x as i16, + }, + )); + } + + if delta.y.abs() > 0.001 { + operations.push(ironrdp::input::Operation::WheelRotations( + ironrdp::input::WheelRotations { + is_vertical: true, + #[expect(clippy::as_conversions, reason = "casting f64 to i16")] + rotation_units: delta.y as i16, + }, + )); + } + } + }; + + let input_events = self.input_database.apply(operations); + + send_fast_path_events(&self.input_event_sender, input_events); + } + WindowEvent::MouseInput { state, button, .. } => { + let mouse_button = match button { + event::MouseButton::Left => ironrdp::input::MouseButton::Left, + event::MouseButton::Right => ironrdp::input::MouseButton::Right, + event::MouseButton::Middle => ironrdp::input::MouseButton::Middle, + event::MouseButton::Back => ironrdp::input::MouseButton::X1, + event::MouseButton::Forward => ironrdp::input::MouseButton::X2, + event::MouseButton::Other(native_button) => { + if let Some(button) = ironrdp::input::MouseButton::from_native_button(native_button) { + button + } else { + return; + } + } + }; + + let operation = match state { + event::ElementState::Pressed => ironrdp::input::Operation::MouseButtonPressed(mouse_button), + event::ElementState::Released => ironrdp::input::Operation::MouseButtonReleased(mouse_button), + }; + + let input_events = self.input_database.apply(core::iter::once(operation)); + + send_fast_path_events(&self.input_event_sender, input_events); + } + WindowEvent::RedrawRequested => { + self.draw(); + } + WindowEvent::ActivationTokenDone { .. } + | WindowEvent::Moved(_) + | WindowEvent::Destroyed + | WindowEvent::HoveredFile(_) + | WindowEvent::HoveredFileCancelled + | WindowEvent::Focused(_) + | WindowEvent::Ime(_) + | WindowEvent::CursorEntered { .. } + | WindowEvent::CursorLeft { .. } + | WindowEvent::PinchGesture { .. } + | WindowEvent::PanGesture { .. } + | WindowEvent::DoubleTapGesture { .. } + | WindowEvent::RotationGesture { .. } + | WindowEvent::TouchpadPressure { .. } + | WindowEvent::AxisMotion { .. } + | WindowEvent::Touch(_) + | WindowEvent::ScaleFactorChanged { .. } + | WindowEvent::ThemeChanged(_) + | WindowEvent::Occluded(_) => { + // ignore + } + } + } + + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RdpOutputEvent) { + let Some((window, surface)) = self.window.as_mut() else { + return; + }; + match event { + RdpOutputEvent::Image { buffer, width, height } => { + trace!(width = ?width, height = ?height, "Received image with size"); + trace!(window_physical_size = ?window.inner_size(), "Drawing image to the window with size"); + self.buffer_size = (width.get(), height.get()); + self.buffer = buffer; + surface + .resize(NonZeroU32::from(width), NonZeroU32::from(height)) + .expect("surface resize"); + + window.request_redraw(); + } + RdpOutputEvent::ConnectionFailure(error) => { + error!(?error); + eprintln!("Connection error: {}", error.report()); + // TODO set proc_exit::sysexits::PROTOCOL_ERR.as_raw()); + event_loop.exit(); + } + RdpOutputEvent::Terminated(result) => { + let _exit_code = match result { + Ok(reason) => { + println!("Terminated gracefully: {reason}"); + proc_exit::sysexits::OK + } + Err(error) => { + error!(?error); + eprintln!("Active session error: {}", error.report()); + proc_exit::sysexits::PROTOCOL_ERR + } + }; + // TODO set exit_code.as_raw()); + event_loop.exit(); + } + RdpOutputEvent::PointerHidden => { + window.set_cursor_visible(false); + } + RdpOutputEvent::PointerDefault => { + window.set_cursor(CursorIcon::default()); + window.set_cursor_visible(true); + } + RdpOutputEvent::PointerPosition { x, y } => { + if let Err(error) = window.set_cursor_position(LogicalPosition::new(x, y)) { + error!(?error, "Failed to set cursor position"); + } + } + RdpOutputEvent::PointerBitmap(pointer) => { + debug!(width = ?pointer.width, height = ?pointer.height, "Received pointer bitmap"); + match CustomCursor::from_rgba( + pointer.bitmap_data.clone(), + pointer.width, + pointer.height, + pointer.hotspot_x, + pointer.hotspot_y, + ) { + Ok(cursor) => window.set_cursor(event_loop.create_custom_cursor(cursor)), + Err(error) => error!(?error, "Failed to set cursor bitmap"), + } + window.set_cursor_visible(true); + } + } + } +} + +fn send_fast_path_events( + input_event_sender: &mpsc::UnboundedSender, + input_events: smallvec::SmallVec<[ironrdp::pdu::input::fast_path::FastPathInputEvent; 2]>, +) { + if !input_events.is_empty() { + let _ = input_event_sender.send(RdpInputEvent::FastPath(input_events)); + } +} diff --git a/crates/ironrdp-client/src/clipboard.rs b/crates/ironrdp-client/src/clipboard.rs new file mode 100644 index 00000000..a58716a9 --- /dev/null +++ b/crates/ironrdp-client/src/clipboard.rs @@ -0,0 +1,25 @@ +use ironrdp::cliprdr::backend::{ClipboardMessage, ClipboardMessageProxy}; +use tokio::sync::mpsc; +use tracing::error; + +use crate::rdp::RdpInputEvent; + +/// Shim for sending and receiving CLIPRDR events as `RdpInputEvent` +#[derive(Clone, Debug)] +pub struct ClientClipboardMessageProxy { + tx: mpsc::UnboundedSender, +} + +impl ClientClipboardMessageProxy { + pub fn new(tx: mpsc::UnboundedSender) -> Self { + Self { tx } + } +} + +impl ClipboardMessageProxy for ClientClipboardMessageProxy { + fn send_clipboard_message(&self, message: ClipboardMessage) { + if self.tx.send(RdpInputEvent::Clipboard(message)).is_err() { + error!("Failed to send os clipboard message, receiver is closed"); + } + } +} diff --git a/crates/ironrdp-client/src/config.rs b/crates/ironrdp-client/src/config.rs new file mode 100644 index 00000000..3477c180 --- /dev/null +++ b/crates/ironrdp-client/src/config.rs @@ -0,0 +1,483 @@ +#![allow(clippy::print_stdout)] + +use core::num::ParseIntError; +use core::str::FromStr; +use std::path::PathBuf; + +use anyhow::Context as _; +use clap::clap_derive::ValueEnum; +use clap::Parser; +use ironrdp::connector::{self, Credentials}; +use ironrdp::pdu::rdp::capability_sets::{client_codecs_capabilities, MajorPlatformType}; +use ironrdp::pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; +use ironrdp_mstsgu::GwConnectTarget; +use tap::prelude::*; +use url::Url; + +const DEFAULT_WIDTH: u16 = 1920; +const DEFAULT_HEIGHT: u16 = 1080; + +#[derive(Clone, Debug)] +pub struct Config { + pub log_file: Option, + pub gw: Option, + pub destination: Destination, + pub connector: connector::Config, + pub clipboard_type: ClipboardType, + pub rdcleanpath: Option, + + /// DVC channel <-> named pipe proxy configuration. + /// + /// Each configured proxy enables IronRDP to connect to DVC channel and create a named pipe + /// server, which will be used for proxying DVC messages to/from user-defined DVC logic + /// implemented as named pipe clients (either in the same process or in a different process). + pub dvc_pipe_proxies: Vec, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum ClipboardType { + Default, + Stub, + #[cfg(windows)] + Windows, + None, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum KeyboardType { + IbmPcXt, + OlivettiIco, + IbmPcAt, + IbmEnhanced, + Nokia1050, + Nokia9140, + Japanese, +} + +impl KeyboardType { + fn parse(keyboard_type: KeyboardType) -> ironrdp::pdu::gcc::KeyboardType { + match keyboard_type { + KeyboardType::IbmEnhanced => ironrdp::pdu::gcc::KeyboardType::IbmEnhanced, + KeyboardType::IbmPcAt => ironrdp::pdu::gcc::KeyboardType::IbmPcAt, + KeyboardType::IbmPcXt => ironrdp::pdu::gcc::KeyboardType::IbmPcXt, + KeyboardType::OlivettiIco => ironrdp::pdu::gcc::KeyboardType::OlivettiIco, + KeyboardType::Nokia1050 => ironrdp::pdu::gcc::KeyboardType::Nokia1050, + KeyboardType::Nokia9140 => ironrdp::pdu::gcc::KeyboardType::Nokia9140, + KeyboardType::Japanese => ironrdp::pdu::gcc::KeyboardType::Japanese, + } + } +} + +fn parse_hex(input: &str) -> Result { + if input.starts_with("0x") { + u32::from_str_radix(input.get(2..).unwrap_or(""), 16) + } else { + input.parse::() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Destination { + name: String, + port: u16, +} + +impl Destination { + pub fn new(addr: impl Into) -> anyhow::Result { + const RDP_DEFAULT_PORT: u16 = 3389; + + let addr = addr.into(); + + if let Some(addr_split) = addr.rsplit_once(':') { + if let Ok(sock_addr) = addr.parse::() { + Ok(Self { + name: sock_addr.ip().to_string(), + port: sock_addr.port(), + }) + } else if addr.parse::().is_ok() { + Ok(Self { + name: addr, + port: RDP_DEFAULT_PORT, + }) + } else { + Ok(Self { + name: addr_split.0.to_owned(), + port: addr_split.1.parse().context("invalid port")?, + }) + } + } else { + Ok(Self { + name: addr, + port: RDP_DEFAULT_PORT, + }) + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn port(&self) -> u16 { + self.port + } +} + +impl FromStr for Destination { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +impl From for connector::ServerName { + fn from(value: Destination) -> Self { + Self::new(value.name) + } +} + +impl From<&Destination> for connector::ServerName { + fn from(value: &Destination) -> Self { + Self::new(&value.name) + } +} + +#[derive(Clone, Debug)] +pub struct RDCleanPathConfig { + pub url: Url, + pub auth_token: String, +} + +#[derive(Clone, Debug)] +pub struct DvcProxyInfo { + pub channel_name: String, + pub pipe_name: String, +} + +impl FromStr for DvcProxyInfo { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut parts = s.split('='); + let channel_name = parts + .next() + .ok_or_else(|| anyhow::anyhow!("missing DVC channel name"))? + .to_owned(); + let pipe_name = parts + .next() + .ok_or_else(|| anyhow::anyhow!("missing DVC proxy pipe name"))? + .to_owned(); + + Ok(Self { + channel_name, + pipe_name, + }) + } +} + +/// Devolutions IronRDP client +#[derive(Parser, Debug)] +#[clap(author = "Devolutions", about = "Devolutions-IronRDP client")] +#[clap(version, long_about = None)] +struct Args { + /// A file with IronRDP client logs + #[clap(short, long, value_parser)] + log_file: Option, + + #[clap(long, value_parser)] + gw_endpoint: Option, + #[clap(long, value_parser)] + gw_user: Option, + #[clap(long, value_parser)] + gw_pass: Option, + + /// An address on which the client will connect. + destination: Option, + + /// Path to a .rdp file to read the configuration from. + #[clap(long)] + rdp_file: Option, + + /// A target RDP server user name + #[clap(short, long)] + username: Option, + + /// An optional target RDP server domain name + #[clap(short, long)] + domain: Option, + + /// A target RDP server user password + #[clap(short, long)] + password: Option, + + /// Proxy URL to connect to for the RDCleanPath + #[clap(long, requires("rdcleanpath_token"))] + rdcleanpath_url: Option, + + /// Authentication token to insert in the RDCleanPath packet + #[clap(long, requires("rdcleanpath_url"))] + rdcleanpath_token: Option, + + /// The keyboard type + #[clap(long, value_enum, default_value_t = KeyboardType::IbmEnhanced)] + keyboard_type: KeyboardType, + + /// The keyboard subtype (an original equipment manufacturer-dependent value) + #[clap(long, default_value_t = 0)] + keyboard_subtype: u32, + + /// The number of function keys on the keyboard + #[clap(long, default_value_t = 12)] + keyboard_functional_keys_count: u32, + + /// The input method editor (IME) file name associated with the active input locale + #[clap(long, default_value_t = String::from(""))] + ime_file_name: String, + + /// Contains a value that uniquely identifies the client + #[clap(long, default_value_t = String::from(""))] + dig_product_id: String, + + /// Enable thin client + #[clap(long)] + thin_client: bool, + + /// Enable small cache + #[clap(long)] + small_cache: bool, + + /// Set required color depth. Currently only 32 and 16 bit color depths are supported + #[clap(long)] + color_depth: Option, + + /// Ignore mouse pointer messages sent by the server. Increases performance when enabled, as the + /// client could skip costly software rendering of the pointer with alpha blending + #[clap(long)] + no_server_pointer: bool, + + /// Enabled capability versions. Each bit represents enabling a capability version + /// starting from V8 to V10_7 + #[clap(long, value_parser = parse_hex, default_value_t = 0)] + capabilities: u32, + + /// Automatically logon to the server by passing the INFO_AUTOLOGON flag + /// + /// This flag is ignored if CredSSP authentication is used. + /// You can use `--no-credssp` to ensure it’s not. + #[clap(long)] + autologon: bool, + + /// Disable TLS + Graphical login (legacy authentication method) + /// + /// Disabling this in order to enforce usage of CredSSP (NLA) is recommended. + #[clap(long)] + no_tls: bool, + + /// Disable TLS + Network Level Authentication (NLA) using CredSSP + /// + /// NLA is used to authenticates RDP clients and servers before sending credentials over the network. + /// It’s not recommended to disable this. + #[clap(long, alias = "no-nla")] + no_credssp: bool, + + /// The clipboard type + #[clap(long, value_enum, default_value_t = ClipboardType::Default)] + clipboard_type: ClipboardType, + + /// The bitmap codecs to use (remotefx:on, ...) + #[clap(long, num_args = 1.., value_delimiter = ',')] + codecs: Vec, + + /// Add DVC channel named pipe proxy + /// + /// The format is `=`, e.g., `ChannelName=PipeName` where `ChannelName` is the name of the channel, + /// and `PipeName` is the name of the named pipe to connect to (without OS-specific prefix). + /// `` will automatically be prefixed with `\\.\pipe\` on Windows. + #[clap(long)] + dvc_proxy: Vec, +} + +impl Config { + pub fn parse_args() -> anyhow::Result { + use ironrdp_cfg::PropertySetExt as _; + + let args = Args::parse(); + + let mut properties = ironrdp_propertyset::PropertySet::new(); + + if let Some(rdp_file) = args.rdp_file { + let input = + std::fs::read_to_string(&rdp_file).with_context(|| format!("failed to read {}", rdp_file.display()))?; + + if let Err(errors) = ironrdp_rdpfile::load(&mut properties, &input) { + for e in errors { + #[expect(clippy::print_stderr)] + { + eprintln!("Error when reading {}: {e}", rdp_file.display()) + } + } + } + } + + let mut gw: Option = None; + if let Some(gw_addr) = args.gw_endpoint { + gw = Some(GwConnectTarget { + gw_endpoint: gw_addr, + gw_user: String::new(), + gw_pass: String::new(), + server: String::new(), // TODO: non-standard port? also dont use here? + }); + } + + if let Some(ref mut gw) = gw { + gw.gw_user = if let Some(gw_user) = args.gw_user { + gw_user + } else { + inquire::Text::new("Gateway username:") + .prompt() + .context("Username prompt")? + }; + + gw.gw_pass = if let Some(gw_pass) = args.gw_pass { + gw_pass + } else { + inquire::Password::new("Gateway password:") + .without_confirmation() + .prompt() + .context("Password prompt")? + }; + }; + + let destination = if let Some(destination) = args.destination { + destination + } else if let Some(destination) = properties.full_address() { + if let Some(port) = properties.server_port() { + format!("{destination}:{port}").parse() + } else { + destination.parse() + } + .context("invalid destination")? + } else { + inquire::Text::new("Server address:") + .prompt() + .context("Address prompt")? + .pipe(Destination::new)? + }; + + if let Some(ref mut gw) = gw { + gw.server = destination.name.clone(); // TODO + } + + let username = if let Some(username) = args.username { + username + } else if let Some(username) = properties.username() { + username.to_owned() + } else { + inquire::Text::new("Username:").prompt().context("Username prompt")? + }; + + let password = if let Some(password) = args.password { + password + } else if let Some(password) = properties.clear_text_password() { + password.to_owned() + } else { + inquire::Password::new("Password:") + .without_confirmation() + .prompt() + .context("Password prompt")? + }; + + let codecs: Vec<_> = args.codecs.iter().map(|s| s.as_str()).collect(); + let codecs = match client_codecs_capabilities(&codecs) { + Ok(codecs) => codecs, + Err(help) => { + print!("{help}"); + std::process::exit(0); + } + }; + let mut bitmap = connector::BitmapConfig { + color_depth: 32, + lossy_compression: true, + codecs, + }; + + if let Some(color_depth) = args.color_depth { + if color_depth != 16 && color_depth != 32 { + anyhow::bail!("Invalid color depth. Only 16 and 32 bit color depths are supported."); + } + bitmap.color_depth = color_depth; + }; + + let clipboard_type = if args.clipboard_type == ClipboardType::Default { + #[cfg(windows)] + { + ClipboardType::Windows + } + #[cfg(not(windows))] + { + ClipboardType::None + } + } else { + args.clipboard_type + }; + + let connector = connector::Config { + credentials: Credentials::UsernamePassword { username, password }, + domain: args.domain, + enable_tls: !args.no_tls, + enable_credssp: !args.no_credssp, + keyboard_type: KeyboardType::parse(args.keyboard_type), + keyboard_subtype: args.keyboard_subtype, + keyboard_layout: 0, // the server SHOULD use the default active input locale identifier + keyboard_functional_keys_count: args.keyboard_functional_keys_count, + ime_file_name: args.ime_file_name, + dig_product_id: args.dig_product_id, + desktop_size: connector::DesktopSize { + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + }, + desktop_scale_factor: 0, // Default to 0 per FreeRDP + bitmap: Some(bitmap), + client_build: semver::Version::parse(env!("CARGO_PKG_VERSION")) + .map_or(0, |version| version.major * 100 + version.minor * 10 + version.patch) + .pipe(u32::try_from) + .context("cargo package version")?, + client_name: whoami::fallible::hostname().unwrap_or_else(|_| "ironrdp".to_owned()), + // NOTE: hardcode this value like in freerdp + // https://github.com/FreeRDP/FreeRDP/blob/4e24b966c86fdf494a782f0dfcfc43a057a2ea60/libfreerdp/core/settings.c#LL49C34-L49C70 + client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), + platform: match whoami::platform() { + whoami::Platform::Windows => MajorPlatformType::WINDOWS, + whoami::Platform::Linux => MajorPlatformType::UNIX, + whoami::Platform::MacOS => MajorPlatformType::MACINTOSH, + whoami::Platform::Ios => MajorPlatformType::IOS, + whoami::Platform::Android => MajorPlatformType::ANDROID, + _ => MajorPlatformType::UNSPECIFIED, + }, + hardware_id: None, + license_cache: None, + enable_server_pointer: !args.no_server_pointer, + autologon: args.autologon, + enable_audio_playback: true, + request_data: None, + pointer_software_rendering: false, + performance_flags: PerformanceFlags::default(), + timezone_info: TimezoneInfo::default(), + }; + + let rdcleanpath = args + .rdcleanpath_url + .zip(args.rdcleanpath_token) + .map(|(url, auth_token)| RDCleanPathConfig { url, auth_token }); + + Ok(Self { + log_file: args.log_file, + gw, + destination, + connector, + clipboard_type, + rdcleanpath, + dvc_pipe_proxies: args.dvc_proxy, + }) + } +} diff --git a/crates/ironrdp-client/src/lib.rs b/crates/ironrdp-client/src/lib.rs new file mode 100644 index 00000000..de4b5276 --- /dev/null +++ b/crates/ironrdp-client/src/lib.rs @@ -0,0 +1,17 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary + +// No need to be as strict as in production libraries +#![allow(clippy::arithmetic_side_effects)] +#![allow(clippy::cast_lossless)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_possible_wrap)] +#![allow(clippy::cast_sign_loss)] + +pub mod app; +pub mod clipboard; +pub mod config; +pub mod rdp; + +mod ws; diff --git a/crates/ironrdp-client/src/main.rs b/crates/ironrdp-client/src/main.rs new file mode 100644 index 00000000..18d41f34 --- /dev/null +++ b/crates/ironrdp-client/src/main.rs @@ -0,0 +1,122 @@ +#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary + +use anyhow::Context as _; +use ironrdp_client::app::App; +use ironrdp_client::config::{ClipboardType, Config}; +use ironrdp_client::rdp::{DvcPipeProxyFactory, RdpClient, RdpInputEvent, RdpOutputEvent}; +use tokio::runtime; +use tracing::debug; +use winit::event_loop::EventLoop; + +fn main() -> anyhow::Result<()> { + let mut config = Config::parse_args().context("CLI arguments parsing")?; + + setup_logging(config.log_file.as_deref()).context("unable to initialize logging")?; + + debug!("Initialize App"); + let event_loop = EventLoop::::with_user_event().build()?; + let event_loop_proxy = event_loop.create_proxy(); + let (input_event_sender, input_event_receiver) = RdpInputEvent::create_channel(); + let mut app = App::new(&event_loop, &input_event_sender).context("unable to initialize App")?; + + // TODO: get window size & scale factor from GUI/App + let window_size = (1024, 768); + config.connector.desktop_scale_factor = 0; + config.connector.desktop_size.width = window_size.0; + config.connector.desktop_size.height = window_size.1; + + let rt = runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("unable to create tokio runtime")?; + + // NOTE: we need to keep `win_clipboard` alive, otherwise it will be dropped before IronRDP + // starts and clipboard functionality will not be available. + #[cfg(windows)] + let _win_clipboard; + + let cliprdr_factory = match config.clipboard_type { + ClipboardType::Stub => { + use ironrdp_cliprdr_native::StubClipboard; + + let cliprdr = StubClipboard::new(); + let factory = cliprdr.backend_factory(); + Some(factory) + } + #[cfg(windows)] + ClipboardType::Windows => { + use ironrdp_client::clipboard::ClientClipboardMessageProxy; + use ironrdp_cliprdr_native::WinClipboard; + + let cliprdr = WinClipboard::new(ClientClipboardMessageProxy::new(input_event_sender.clone()))?; + + let factory = cliprdr.backend_factory(); + _win_clipboard = cliprdr; + Some(factory) + } + _ => None, + }; + + let dvc_pipe_proxy_factory = DvcPipeProxyFactory::new(input_event_sender); + + let client = RdpClient { + config, + event_loop_proxy, + input_event_receiver, + cliprdr_factory, + dvc_pipe_proxy_factory, + }; + + debug!("Start RDP thread"); + std::thread::spawn(move || { + rt.block_on(client.run()); + }); + + debug!("Run App"); + event_loop.run_app(&mut app)?; + Ok(()) +} + +fn setup_logging(log_file: Option<&str>) -> anyhow::Result<()> { + use std::fs::OpenOptions; + + use tracing::metadata::LevelFilter; + use tracing_subscriber::prelude::*; + use tracing_subscriber::EnvFilter; + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .with_env_var("IRONRDP_LOG") + .from_env_lossy(); + + if let Some(log_file) = log_file { + let file = OpenOptions::new() + .create(true) + .append(true) + .open(log_file) + .with_context(|| format!("couldn't open {log_file}"))?; + let fmt_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_writer(file) + .compact(); + tracing_subscriber::registry() + .with(env_filter) + .with(fmt_layer) + .try_init() + .context("failed to set tracing global subscriber")?; + } else { + let fmt_layer = tracing_subscriber::fmt::layer() + .compact() + .with_file(true) + .with_line_number(true) + .with_thread_ids(true) + .with_target(false); + tracing_subscriber::registry() + .with(env_filter) + .with(fmt_layer) + .try_init() + .context("failed to set tracing global subscriber")?; + }; + + Ok(()) +} diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs new file mode 100644 index 00000000..a6693fba --- /dev/null +++ b/crates/ironrdp-client/src/rdp.rs @@ -0,0 +1,691 @@ +use core::num::NonZeroU16; +use std::sync::Arc; + +use ironrdp::cliprdr::backend::{ClipboardMessage, CliprdrBackendFactory}; +use ironrdp::connector::connection_activation::ConnectionActivationState; +use ironrdp::connector::{ConnectionResult, ConnectorResult}; +use ironrdp::displaycontrol::client::DisplayControlClient; +use ironrdp::displaycontrol::pdu::MonitorLayoutEntry; +use ironrdp::graphics::image_processing::PixelFormat; +use ironrdp::graphics::pointer::DecodedPointer; +use ironrdp::pdu::input::fast_path::FastPathInputEvent; +use ironrdp::pdu::{pdu_other_err, PduResult}; +use ironrdp::session::image::DecodedImage; +use ironrdp::session::{fast_path, ActiveStage, ActiveStageOutput, GracefulDisconnectReason, SessionResult}; +use ironrdp::svc::SvcMessage; +use ironrdp::{cliprdr, connector, rdpdr, rdpsnd, session}; +use ironrdp_core::WriteBuf; +use ironrdp_dvc_pipe_proxy::DvcNamedPipeProxy; +use ironrdp_rdpsnd_native::cpal; +use ironrdp_tokio::reqwest::ReqwestNetworkClient; +use ironrdp_tokio::{single_sequence_step_read, split_tokio_framed, FramedWrite}; +use rdpdr::NoopRdpdrBackend; +use smallvec::SmallVec; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpStream; +use tokio::sync::mpsc; +use tracing::{debug, error, info, trace, warn}; +use winit::event_loop::EventLoopProxy; + +use crate::config::{Config, RDCleanPathConfig}; + +#[derive(Debug)] +pub enum RdpOutputEvent { + Image { + buffer: Vec, + width: NonZeroU16, + height: NonZeroU16, + }, + ConnectionFailure(connector::ConnectorError), + PointerDefault, + PointerHidden, + PointerPosition { + x: u16, + y: u16, + }, + PointerBitmap(Arc), + Terminated(SessionResult), +} + +#[derive(Debug)] +pub enum RdpInputEvent { + Resize { + width: u16, + height: u16, + scale_factor: u32, + /// The physical size of the display in millimeters (width, height). + physical_size: Option<(u32, u32)>, + }, + FastPath(SmallVec<[FastPathInputEvent; 2]>), + Close, + Clipboard(ClipboardMessage), + SendDvcMessages { + channel_id: u32, + messages: Vec, + }, +} + +impl RdpInputEvent { + pub fn create_channel() -> (mpsc::UnboundedSender, mpsc::UnboundedReceiver) { + mpsc::unbounded_channel() + } +} + +pub struct DvcPipeProxyFactory { + rdp_input_sender: mpsc::UnboundedSender, +} + +impl DvcPipeProxyFactory { + pub fn new(rdp_input_sender: mpsc::UnboundedSender) -> Self { + Self { rdp_input_sender } + } + + pub fn create(&self, channel_name: String, pipe_name: String) -> DvcNamedPipeProxy { + let rdp_input_sender = self.rdp_input_sender.clone(); + + DvcNamedPipeProxy::new(&channel_name, &pipe_name, move |channel_id, messages| { + rdp_input_sender + .send(RdpInputEvent::SendDvcMessages { channel_id, messages }) + .map_err(|_error| pdu_other_err!("send DVC messages to the event loop",))?; + + Ok(()) + }) + } +} + +pub type WriteDvcMessageFn = Box PduResult<()> + Send + 'static>; + +pub struct RdpClient { + pub config: Config, + pub event_loop_proxy: EventLoopProxy, + pub input_event_receiver: mpsc::UnboundedReceiver, + pub cliprdr_factory: Option>, + pub dvc_pipe_proxy_factory: DvcPipeProxyFactory, +} + +impl RdpClient { + pub async fn run(mut self) { + loop { + let (connection_result, framed) = if let Some(rdcleanpath) = self.config.rdcleanpath.as_ref() { + match connect_ws( + &self.config, + rdcleanpath, + self.cliprdr_factory.as_deref(), + &self.dvc_pipe_proxy_factory, + ) + .await + { + Ok(result) => result, + Err(e) => { + let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e)); + break; + } + } + } else { + match connect( + &self.config, + self.cliprdr_factory.as_deref(), + &self.dvc_pipe_proxy_factory, + ) + .await + { + Ok(result) => result, + Err(e) => { + let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e)); + break; + } + } + }; + + match active_session( + framed, + connection_result, + &self.event_loop_proxy, + &mut self.input_event_receiver, + ) + .await + { + Ok(RdpControlFlow::ReconnectWithNewSize { width, height }) => { + self.config.connector.desktop_size.width = width; + self.config.connector.desktop_size.height = height; + } + Ok(RdpControlFlow::TerminatedGracefully(reason)) => { + let _ = self.event_loop_proxy.send_event(RdpOutputEvent::Terminated(Ok(reason))); + break; + } + Err(e) => { + let _ = self.event_loop_proxy.send_event(RdpOutputEvent::Terminated(Err(e))); + break; + } + } + } + } +} + +enum RdpControlFlow { + ReconnectWithNewSize { width: u16, height: u16 }, + TerminatedGracefully(GracefulDisconnectReason), +} + +trait AsyncReadWrite: AsyncRead + AsyncWrite {} + +impl AsyncReadWrite for T where T: AsyncRead + AsyncWrite {} + +type UpgradedFramed = ironrdp_tokio::TokioFramed>; + +async fn connect( + config: &Config, + cliprdr_factory: Option<&(dyn CliprdrBackendFactory + Send)>, + dvc_pipe_proxy_factory: &DvcPipeProxyFactory, +) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> { + let dest = format!("{}:{}", config.destination.name(), config.destination.port()); + + let (client_addr, stream) = if let Some(ref gw_config) = config.gw { + let (gw, client_addr) = ironrdp_mstsgu::GwClient::connect(gw_config, &config.connector.client_name) + .await + .map_err(|e| connector::custom_err!("GW Connect", e))?; + (client_addr, tokio_util::either::Either::Left(gw)) + } else { + let stream = TcpStream::connect(dest) + .await + .map_err(|e| connector::custom_err!("TCP connect", e))?; + let client_addr = stream + .local_addr() + .map_err(|e| connector::custom_err!("get socket local address", e))?; + (client_addr, tokio_util::either::Either::Right(stream)) + }; + let mut framed = ironrdp_tokio::TokioFramed::new(stream); + + let mut drdynvc = + ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))); + + // Instantiate all DVC proxies + for proxy in config.dvc_pipe_proxies.iter() { + let channel_name = proxy.channel_name.clone(); + let pipe_name = proxy.pipe_name.clone(); + + trace!(%channel_name, %pipe_name, "Creating DVC proxy"); + + drdynvc = drdynvc.with_dynamic_channel(dvc_pipe_proxy_factory.create(channel_name, pipe_name)); + } + + let mut connector = connector::ClientConnector::new(config.connector.clone(), client_addr) + .with_static_channel(drdynvc) + .with_static_channel(rdpsnd::client::Rdpsnd::new(Box::new(cpal::RdpsndBackend::new()))) + .with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0)); + + if let Some(builder) = cliprdr_factory { + let backend = builder.build_cliprdr_backend(); + + let cliprdr = cliprdr::Cliprdr::new(backend); + + connector.attach_static_channel(cliprdr); + } + + let should_upgrade = ironrdp_tokio::connect_begin(&mut framed, &mut connector).await?; + + debug!("TLS upgrade"); + + // Ensure there is no leftover + let (initial_stream, leftover_bytes) = framed.into_inner(); + + let (upgraded_stream, tls_cert) = ironrdp_tls::upgrade(initial_stream, config.destination.name()) + .await + .map_err(|e| connector::custom_err!("TLS upgrade", e))?; + + let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, &mut connector); + + let erased_stream: Box = Box::new(upgraded_stream); + let mut upgraded_framed = ironrdp_tokio::TokioFramed::new_with_leftover(erased_stream, leftover_bytes); + + let server_public_key = ironrdp_tls::extract_tls_server_public_key(&tls_cert) + .ok_or_else(|| connector::general_err!("unable to extract tls server public key"))?; + let connection_result = ironrdp_tokio::connect_finalize( + upgraded, + connector, + &mut upgraded_framed, + &mut ReqwestNetworkClient::new(), + (&config.destination).into(), + server_public_key.to_owned(), + None, + ) + .await?; + + debug!(?connection_result); + + Ok((connection_result, upgraded_framed)) +} + +async fn connect_ws( + config: &Config, + rdcleanpath: &RDCleanPathConfig, + cliprdr_factory: Option<&(dyn CliprdrBackendFactory + Send)>, + dvc_pipe_proxy_factory: &DvcPipeProxyFactory, +) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> { + let hostname = rdcleanpath + .url + .host_str() + .ok_or_else(|| connector::general_err!("host missing from the URL"))?; + + let port = rdcleanpath.url.port_or_known_default().unwrap_or(443); + + let socket = TcpStream::connect((hostname, port)) + .await + .map_err(|e| connector::custom_err!("TCP connect", e))?; + + socket + .set_nodelay(true) + .map_err(|e| connector::custom_err!("set TCP_NODELAY", e))?; + + let client_addr = socket + .local_addr() + .map_err(|e| connector::custom_err!("get socket local address", e))?; + + let (ws, _) = tokio_tungstenite::client_async_tls(rdcleanpath.url.as_str(), socket) + .await + .map_err(|e| connector::custom_err!("WS connect", e))?; + + let ws = crate::ws::websocket_compat(ws); + + let mut framed = ironrdp_tokio::TokioFramed::new(ws); + + let mut drdynvc = + ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))); + + // Instantiate all DVC proxies + for proxy in config.dvc_pipe_proxies.iter() { + let channel_name = proxy.channel_name.clone(); + let pipe_name = proxy.pipe_name.clone(); + + trace!(%channel_name, %pipe_name, "Creating DVC proxy"); + + drdynvc = drdynvc.with_dynamic_channel(dvc_pipe_proxy_factory.create(channel_name, pipe_name)); + } + + let mut connector = connector::ClientConnector::new(config.connector.clone(), client_addr) + .with_static_channel(drdynvc) + .with_static_channel(rdpsnd::client::Rdpsnd::new(Box::new(cpal::RdpsndBackend::new()))) + .with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0)); + + if let Some(builder) = cliprdr_factory { + let backend = builder.build_cliprdr_backend(); + + let cliprdr = cliprdr::Cliprdr::new(backend); + + connector.attach_static_channel(cliprdr); + } + + let destination = format!("{}:{}", config.destination.name(), config.destination.port()); + + let (upgraded, server_public_key) = connect_rdcleanpath( + &mut framed, + &mut connector, + destination, + rdcleanpath.auth_token.clone(), + None, + ) + .await?; + + let connection_result = ironrdp_tokio::connect_finalize( + upgraded, + connector, + &mut framed, + &mut ReqwestNetworkClient::new(), + (&config.destination).into(), + server_public_key, + None, + ) + .await?; + + let (ws, leftover_bytes) = framed.into_inner(); + let erased_stream: Box = Box::new(ws); + let upgraded_framed = ironrdp_tokio::TokioFramed::new_with_leftover(erased_stream, leftover_bytes); + + Ok((connection_result, upgraded_framed)) +} + +async fn connect_rdcleanpath( + framed: &mut ironrdp_tokio::Framed, + connector: &mut connector::ClientConnector, + destination: String, + proxy_auth_token: String, + pcb: Option, +) -> ConnectorResult<(ironrdp_tokio::Upgraded, Vec)> +where + S: ironrdp_tokio::FramedRead + FramedWrite, +{ + use ironrdp::connector::Sequence as _; + use x509_cert::der::Decode as _; + + #[derive(Clone, Copy, Debug)] + struct RDCleanPathHint; + + const RDCLEANPATH_HINT: RDCleanPathHint = RDCleanPathHint; + + impl ironrdp::pdu::PduHint for RDCleanPathHint { + fn find_size(&self, bytes: &[u8]) -> ironrdp::core::DecodeResult> { + match ironrdp_rdcleanpath::RDCleanPathPdu::detect(bytes) { + ironrdp_rdcleanpath::DetectionResult::Detected { total_length, .. } => Ok(Some((true, total_length))), + ironrdp_rdcleanpath::DetectionResult::NotEnoughBytes => Ok(None), + ironrdp_rdcleanpath::DetectionResult::Failed => Err(ironrdp::core::other_err!( + "RDCleanPathHint", + "detection failed (invalid PDU)" + )), + } + } + } + + let mut buf = WriteBuf::new(); + + info!("Begin connection procedure"); + + { + // RDCleanPath request + + let connector::ClientConnectorState::ConnectionInitiationSendRequest = connector.state else { + return Err(connector::general_err!("invalid connector state (send request)")); + }; + + debug_assert!(connector.next_pdu_hint().is_none()); + + let written = connector.step_no_input(&mut buf)?; + let x224_pdu_len = written.size().expect("written size"); + debug_assert_eq!(x224_pdu_len, buf.filled_len()); + let x224_pdu = buf.filled().to_vec(); + + let rdcleanpath_req = + ironrdp_rdcleanpath::RDCleanPathPdu::new_request(x224_pdu, destination, proxy_auth_token, pcb) + .map_err(|e| connector::custom_err!("new RDCleanPath request", e))?; + debug!(message = ?rdcleanpath_req, "Send RDCleanPath request"); + let rdcleanpath_req = rdcleanpath_req + .to_der() + .map_err(|e| connector::custom_err!("RDCleanPath request encode", e))?; + + framed + .write_all(&rdcleanpath_req) + .await + .map_err(|e| connector::custom_err!("couldn't write RDCleanPath request", e))?; + } + + { + // RDCleanPath response + + let rdcleanpath_res = framed + .read_by_hint(&RDCLEANPATH_HINT) + .await + .map_err(|e| connector::custom_err!("read RDCleanPath request", e))?; + + let rdcleanpath_res = ironrdp_rdcleanpath::RDCleanPathPdu::from_der(&rdcleanpath_res) + .map_err(|e| connector::custom_err!("RDCleanPath response decode", e))?; + + debug!(message = ?rdcleanpath_res, "Received RDCleanPath PDU"); + + let (x224_connection_response, server_cert_chain) = match rdcleanpath_res + .into_enum() + .map_err(|e| connector::custom_err!("invalid RDCleanPath PDU", e))? + { + ironrdp_rdcleanpath::RDCleanPath::Request { .. } => { + return Err(connector::general_err!( + "received an unexpected RDCleanPath type (request)", + )); + } + ironrdp_rdcleanpath::RDCleanPath::Response { + x224_connection_response, + server_cert_chain, + server_addr: _, + } => (x224_connection_response, server_cert_chain), + ironrdp_rdcleanpath::RDCleanPath::GeneralErr(error) => { + return Err(connector::custom_err!("received an RDCleanPath error", error)); + } + ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { + x224_connection_response, + } => { + // Try to decode as X.224 Connection Confirm to extract negotiation failure details. + if let Ok(x224_confirm) = ironrdp_core::decode::< + ironrdp::pdu::x224::X224, + >(&x224_connection_response) + { + if let ironrdp::pdu::nego::ConnectionConfirm::Failure { code } = x224_confirm.0 { + // Convert to negotiation failure instead of generic RDCleanPath error. + let negotiation_failure = connector::NegotiationFailure::from(code); + return Err(connector::ConnectorError::new( + "RDP negotiation failed", + connector::ConnectorErrorKind::Negotiation(negotiation_failure), + )); + } + } + + // Fallback to generic error if we can't decode the negotiation failure. + return Err(connector::general_err!("received an RDCleanPath negotiation error")); + } + }; + + let connector::ClientConnectorState::ConnectionInitiationWaitConfirm { .. } = connector.state else { + return Err(connector::general_err!("invalid connector state (wait confirm)")); + }; + + debug_assert!(connector.next_pdu_hint().is_some()); + + buf.clear(); + let written = connector.step(x224_connection_response.as_bytes(), &mut buf)?; + + debug_assert!(written.is_nothing()); + + let server_cert = server_cert_chain + .into_iter() + .next() + .ok_or_else(|| connector::general_err!("server cert chain missing from rdcleanpath response"))?; + + let cert = x509_cert::Certificate::from_der(server_cert.as_bytes()) + .map_err(|e| connector::custom_err!("server cert chain missing from rdcleanpath response", e))?; + + let server_public_key = cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() + .ok_or_else(|| connector::general_err!("subject public key BIT STRING is not aligned"))? + .to_owned(); + + let should_upgrade = ironrdp_tokio::skip_connect_begin(connector); + + // At this point, proxy established the TLS session. + + let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, connector); + + Ok((upgraded, server_public_key)) + } +} + +async fn active_session( + framed: UpgradedFramed, + connection_result: ConnectionResult, + event_loop_proxy: &EventLoopProxy, + input_event_receiver: &mut mpsc::UnboundedReceiver, +) -> SessionResult { + let (mut reader, mut writer) = split_tokio_framed(framed); + let mut image = DecodedImage::new( + PixelFormat::RgbA32, + connection_result.desktop_size.width, + connection_result.desktop_size.height, + ); + + let mut active_stage = ActiveStage::new(connection_result); + + let disconnect_reason = 'outer: loop { + let outputs = tokio::select! { + frame = reader.read_pdu() => { + let (action, payload) = frame.map_err(|e| session::custom_err!("read frame", e))?; + trace!(?action, frame_length = payload.len(), "Frame received"); + + active_stage.process(&mut image, action, &payload)? + } + input_event = input_event_receiver.recv() => { + let input_event = input_event.ok_or_else(|| session::general_err!("GUI is stopped"))?; + + match input_event { + RdpInputEvent::Resize { width, height, scale_factor, physical_size } => { + trace!(width, height, "Resize event"); + let width = u32::from(width); + let height = u32::from(height); + // TODO: Make adjust_display_size take and return width and height as u16. + // From the function's doc comment, the width and height values must be less than or equal to 8192 pixels. + // Therefore, we can remove unnecessary casts from u16 to u32 and back. + let (width, height) = MonitorLayoutEntry::adjust_display_size(width, height); + debug!(width, height, "Adjusted display size"); + if let Some(response_frame) = active_stage.encode_resize(width, height, Some(scale_factor), physical_size) { + vec![ActiveStageOutput::ResponseFrame(response_frame?)] + } else { + // TODO(#271): use the "auto-reconnect cookie": https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/15b0d1c9-2891-4adb-a45e-deb4aeeeab7c + debug!("Reconnecting with new size"); + let width = u16::try_from(width).expect("always in the range"); + let height = u16::try_from(height).expect("always in the range"); + return Ok(RdpControlFlow::ReconnectWithNewSize { width, height }) + } + }, + RdpInputEvent::FastPath(events) => { + trace!(?events); + active_stage.process_fastpath_input(&mut image, &events)? + } + RdpInputEvent::Close => { + active_stage.graceful_shutdown()? + } + RdpInputEvent::Clipboard(event) => { + if let Some(cliprdr) = active_stage.get_svc_processor::() { + if let Some(svc_messages) = match event { + ClipboardMessage::SendInitiateCopy(formats) => { + Some(cliprdr.initiate_copy(&formats) + .map_err(|e| session::custom_err!("CLIPRDR", e))?) + } + ClipboardMessage::SendFormatData(response) => { + Some(cliprdr.submit_format_data(response) + .map_err(|e| session::custom_err!("CLIPRDR", e))?) + } + ClipboardMessage::SendInitiatePaste(format) => { + Some(cliprdr.initiate_paste(format) + .map_err(|e| session::custom_err!("CLIPRDR", e))?) + } + ClipboardMessage::Error(e) => { + error!("Clipboard backend error: {}", e); + None + } + } { + let frame = active_stage.process_svc_processor_messages(svc_messages)?; + // Send the messages to the server + vec![ActiveStageOutput::ResponseFrame(frame)] + } else { + // No messages to send to the server + Vec::new() + } + } else { + warn!("Clipboard event received, but Cliprdr is not available"); + Vec::new() + } + } + RdpInputEvent::SendDvcMessages { channel_id, messages } => { + trace!(channel_id, ?messages, "Send DVC messages"); + + let frame = active_stage.encode_dvc_messages(messages)?; + vec![ActiveStageOutput::ResponseFrame(frame)] + } + } + } + }; + + for out in outputs { + match out { + ActiveStageOutput::ResponseFrame(frame) => writer + .write_all(&frame) + .await + .map_err(|e| session::custom_err!("write response", e))?, + ActiveStageOutput::GraphicsUpdate(_region) => { + let buffer: Vec = image + .data() + .chunks_exact(4) + .map(|pixel| { + let r = pixel[0]; + let g = pixel[1]; + let b = pixel[2]; + u32::from_be_bytes([0, r, g, b]) + }) + .collect(); + + event_loop_proxy + .send_event(RdpOutputEvent::Image { + buffer, + width: NonZeroU16::new(image.width()) + .ok_or_else(|| session::general_err!("width is zero"))?, + height: NonZeroU16::new(image.height()) + .ok_or_else(|| session::general_err!("height is zero"))?, + }) + .map_err(|e| session::custom_err!("event_loop_proxy", e))?; + } + ActiveStageOutput::PointerDefault => { + event_loop_proxy + .send_event(RdpOutputEvent::PointerDefault) + .map_err(|e| session::custom_err!("event_loop_proxy", e))?; + } + ActiveStageOutput::PointerHidden => { + event_loop_proxy + .send_event(RdpOutputEvent::PointerHidden) + .map_err(|e| session::custom_err!("event_loop_proxy", e))?; + } + ActiveStageOutput::PointerPosition { x, y } => { + event_loop_proxy + .send_event(RdpOutputEvent::PointerPosition { x, y }) + .map_err(|e| session::custom_err!("event_loop_proxy", e))?; + } + ActiveStageOutput::PointerBitmap(pointer) => { + event_loop_proxy + .send_event(RdpOutputEvent::PointerBitmap(pointer)) + .map_err(|e| session::custom_err!("event_loop_proxy", e))?; + } + ActiveStageOutput::DeactivateAll(mut connection_activation) => { + // Execute the Deactivation-Reactivation Sequence: + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432 + debug!("Received Server Deactivate All PDU, executing Deactivation-Reactivation Sequence"); + let mut buf = WriteBuf::new(); + 'activation_seq: loop { + let written = single_sequence_step_read(&mut reader, &mut *connection_activation, &mut buf) + .await + .map_err(|e| session::custom_err!("read deactivation-reactivation sequence step", e))?; + + if written.size().is_some() { + writer.write_all(buf.filled()).await.map_err(|e| { + session::custom_err!("write deactivation-reactivation sequence step", e) + })?; + } + + if let ConnectionActivationState::Finalized { + io_channel_id, + user_channel_id, + desktop_size, + enable_server_pointer, + pointer_software_rendering, + } = connection_activation.connection_activation_state() + { + debug!(?desktop_size, "Deactivation-Reactivation Sequence completed"); + // Update image size with the new desktop size. + image = DecodedImage::new(PixelFormat::RgbA32, desktop_size.width, desktop_size.height); + // Update the active stage with the new channel IDs and pointer settings. + active_stage.set_fastpath_processor( + fast_path::ProcessorBuilder { + io_channel_id, + user_channel_id, + enable_server_pointer, + pointer_software_rendering, + } + .build(), + ); + active_stage.set_enable_server_pointer(enable_server_pointer); + break 'activation_seq; + } + } + } + ActiveStageOutput::Terminate(reason) => break 'outer reason, + } + } + }; + + Ok(RdpControlFlow::TerminatedGracefully(disconnect_reason)) +} diff --git a/crates/ironrdp-client/src/ws.rs b/crates/ironrdp-client/src/ws.rs new file mode 100644 index 00000000..675553b9 --- /dev/null +++ b/crates/ironrdp-client/src/ws.rs @@ -0,0 +1,34 @@ +use futures_util::{Sink, SinkExt as _, Stream, StreamExt as _}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_tungstenite::tungstenite; + +pub(crate) fn websocket_compat(stream: S) -> impl AsyncRead + AsyncWrite + Unpin + Send + 'static +where + S: Stream> + + Sink + + Unpin + + Send + + 'static, +{ + let compat = stream + .filter_map(|item| { + let mapped = item + .map(|msg| match msg { + tungstenite::Message::Text(s) => Some(transport::WsReadMsg::Payload(tungstenite::Bytes::from(s))), + tungstenite::Message::Binary(data) => Some(transport::WsReadMsg::Payload(data)), + tungstenite::Message::Ping(_) | tungstenite::Message::Pong(_) => None, + tungstenite::Message::Close(_) => Some(transport::WsReadMsg::Close), + tungstenite::Message::Frame(_) => unreachable!("raw frames are never returned when reading"), + }) + .transpose(); + + core::future::ready(mapped) + }) + .with(|item| { + core::future::ready(Ok::<_, tungstenite::Error>(tungstenite::Message::Binary( + tungstenite::Bytes::from(item), + ))) + }); + + transport::WsStream::new(compat) +} diff --git a/crates/ironrdp-cliprdr-format/CHANGELOG.md b/crates/ironrdp-cliprdr-format/CHANGELOG.md new file mode 100644 index 00000000..717d9c1c --- /dev/null +++ b/crates/ironrdp-cliprdr-format/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.1.4](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-format-v0.1.3...ironrdp-cliprdr-format-v0.1.4)] - 2025-09-04 + +### Build + +- Bump png from 0.17.16 to 0.18.0 (#961) ([21fa028dff](https://github.com/Devolutions/IronRDP/commit/21fa028dffa5f9bb1498b4d48d063ea42929faf5)) + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-format-v0.1.2...ironrdp-cliprdr-format-v0.1.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-format-v0.1.1...ironrdp-cliprdr-format-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + diff --git a/crates/ironrdp-cliprdr-format/Cargo.toml b/crates/ironrdp-cliprdr-format/Cargo.toml new file mode 100644 index 00000000..e5d4c4fb --- /dev/null +++ b/crates/ironrdp-cliprdr-format/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ironrdp-cliprdr-format" +version = "0.1.4" +readme = "README.md" +description = "CLIPRDR format conversion library" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["std"] } # public +png = "0.18" + +[lints] +workspace = true diff --git a/crates/ironrdp-cliprdr-format/LICENSE-APACHE b/crates/ironrdp-cliprdr-format/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-cliprdr-format/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-cliprdr-format/LICENSE-MIT b/crates/ironrdp-cliprdr-format/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-cliprdr-format/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-cliprdr-format/README.md b/crates/ironrdp-cliprdr-format/README.md new file mode 100644 index 00000000..aa0f3ac0 --- /dev/null +++ b/crates/ironrdp-cliprdr-format/README.md @@ -0,0 +1,16 @@ +# IronRDP CLIPRDR formats decoding/encoding library + +This Library provides the conversion logic between RDP-specific clipboard formats and +widely used formats like PNG for images, plain string for HTML etc. + +### Overflows + +This crate has been audited by us and is guaranteed overflow-free on 32 and 64 bits architectures. +It would be easy to cause an overflow on a 16-bit architecture. +However, it’s hard to imagine an RDP client running on such machines. +Size of pointers on such architectures greatly limits the maximum size of the bitmap buffers. +It’s likely the RDP client will choke on a big payload before overflowing because of this crate. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-cliprdr-format/src/bitmap.rs b/crates/ironrdp-cliprdr-format/src/bitmap.rs new file mode 100644 index 00000000..0dfcab86 --- /dev/null +++ b/crates/ironrdp-cliprdr-format/src/bitmap.rs @@ -0,0 +1,839 @@ +use std::io::Cursor; + +use ironrdp_core::{ + cast_int, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +/// Maximum size of PNG image that could be placed on the clipboard. +const MAX_BUFFER_SIZE: usize = 64 * 1024 * 1024; // 64 MB + +#[derive(Debug)] +pub enum BitmapError { + Decode(ironrdp_core::DecodeError), + Encode(ironrdp_core::EncodeError), + Unsupported(&'static str), + InvalidSize, + BufferTooBig, + WidthTooBig, + HeightTooBig, + PngEncode(png::EncodingError), + PngDecode(png::DecodingError), +} + +impl core::fmt::Display for BitmapError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + BitmapError::Decode(_error) => write!(f, "decoding error"), + BitmapError::Encode(_error) => write!(f, "encoding error"), + BitmapError::Unsupported(s) => write!(f, "unsupported bitmap: {s}"), + BitmapError::InvalidSize => write!(f, "one of bitmap's dimensions is invalid"), + BitmapError::BufferTooBig => write!(f, "buffer size required for allocation is too big"), + BitmapError::WidthTooBig => write!(f, "image width is too big"), + BitmapError::HeightTooBig => write!(f, "image height is too big"), + BitmapError::PngEncode(_error) => write!(f, "PNG encoding error"), + BitmapError::PngDecode(_error) => write!(f, "PNG decoding error"), + } + } +} + +impl core::error::Error for BitmapError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + BitmapError::Decode(error) => Some(error), + BitmapError::Encode(error) => Some(error), + BitmapError::Unsupported(_) => None, + BitmapError::InvalidSize => None, + BitmapError::BufferTooBig => None, + BitmapError::WidthTooBig => None, + BitmapError::HeightTooBig => None, + BitmapError::PngEncode(encoding_error) => Some(encoding_error), + BitmapError::PngDecode(decoding_error) => Some(decoding_error), + } + } +} + +impl From for BitmapError { + fn from(error: png::EncodingError) -> Self { + BitmapError::PngEncode(error) + } +} + +impl From for BitmapError { + fn from(error: png::DecodingError) -> Self { + BitmapError::PngDecode(error) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct BitmapCompression(u32); + +#[expect(dead_code)] +impl BitmapCompression { + const RGB: Self = Self(0x0000); + const RLE8: Self = Self(0x0001); + const RLE4: Self = Self(0x0002); + const BITFIELDS: Self = Self(0x0003); + const JPEG: Self = Self(0x0004); + const PNG: Self = Self(0x0005); + const CMYK: Self = Self(0x000B); + const CMYKRLE8: Self = Self(0x000C); + const CMYKRLE4: Self = Self(0x000D); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct ColorSpace(u32); + +#[expect(dead_code)] +impl ColorSpace { + const CALIBRATED_RGB: Self = Self(0x00000000); + const SRGB: Self = Self(0x73524742); + const WINDOWS: Self = Self(0x57696E20); + const PROFILE_LINKED: Self = Self(0x4C494E4B); + const PROFILE_EMBEDDED: Self = Self(0x4D424544); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct BitmapIntent(u32); + +#[expect(dead_code)] +impl BitmapIntent { + const LCS_GM_ABS_COLORIMETRIC: Self = Self(0x00000008); + const LCS_GM_BUSINESS: Self = Self(0x00000001); + const LCS_GM_GRAPHICS: Self = Self(0x00000002); + const LCS_GM_IMAGES: Self = Self(0x00000004); +} + +type Fxpt2Dot30 = u32; // (LONG) + +#[derive(Default)] +struct Ciexyz { + x: Fxpt2Dot30, + y: Fxpt2Dot30, + z: Fxpt2Dot30, +} + +#[derive(Default)] +struct CiexyzTriple { + red: Ciexyz, + green: Ciexyz, + blue: Ciexyz, +} + +impl CiexyzTriple { + const NAME: &'static str = "CIEXYZTRIPLE"; + const FIXED_PART_SIZE: usize = 4 * 3 * 3; // 4(LONG) * 3(xyz) * 3(red, green, blue) +} + +impl<'a> Decode<'a> for CiexyzTriple { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let red = Ciexyz { + x: src.read_u32(), + y: src.read_u32(), + z: src.read_u32(), + }; + + let green = Ciexyz { + x: src.read_u32(), + y: src.read_u32(), + z: src.read_u32(), + }; + + let blue = Ciexyz { + x: src.read_u32(), + y: src.read_u32(), + z: src.read_u32(), + }; + + Ok(Self { red, green, blue }) + } +} + +impl Encode for CiexyzTriple { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.red.x); + dst.write_u32(self.red.y); + dst.write_u32(self.red.z); + + dst.write_u32(self.green.x); + dst.write_u32(self.green.y); + dst.write_u32(self.green.z); + + dst.write_u32(self.blue.x); + dst.write_u32(self.blue.y); + dst.write_u32(self.blue.z); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +/// Header used in `CF_DIB` formats, part of [BITMAPINFO] +/// +/// We don't use the optional `bmiColors` field, because it is only relevant for bitmaps with +/// bpp < 24, which are not supported yet, therefore only fixed part of the header is implemented. +/// +/// [BITMAPINFO]: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct BitmapInfoHeader { + /// INVARIANT: `width.abs() <= 10_000` + width: i32, + /// INVARIANT: `height.abs() <= 10_000` + height: i32, + /// INVARIANT: `bit_count <= 32` + bit_count: u16, + compression: BitmapCompression, + size_image: u32, + x_pels_per_meter: i32, + y_pels_per_meter: i32, + clr_used: u32, + clr_important: u32, +} + +impl BitmapInfoHeader { + const FIXED_PART_SIZE: usize = 4 // biSize (DWORD) + + 4 // biWidth (LONG) + + 4 // biHeight (LONG) + + 2 // biPlanes (WORD) + + 2 // biBitCount (WORD) + + 4 // biCompression (DWORD) + + 4 // biSizeImage (DWORD) + + 4 // biXPelsPerMeter (LONG) + + 4 // biYPelsPerMeter (LONG) + + 4 // biClrUsed (DWORD) + + 4; // biClrImportant (DWORD) + + const NAME: &'static str = "BITMAPINFOHEADER"; + + fn encode_with_size(&self, dst: &mut WriteCursor<'_>, size: u32) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(size); + dst.write_i32(self.width); + dst.write_i32(self.height); + dst.write_u16(1); // biPlanes + dst.write_u16(self.bit_count); + dst.write_u32(self.compression.0); + dst.write_u32(self.size_image); + dst.write_i32(self.x_pels_per_meter); + dst.write_i32(self.y_pels_per_meter); + dst.write_u32(self.clr_used); + dst.write_u32(self.clr_important); + + Ok(()) + } + + fn decode_with_size(src: &mut ReadCursor<'_>) -> DecodeResult<(Self, u32)> { + ensure_fixed_part_size!(in: src); + + let size = src.read_u32(); + + // NOTE: .abs() could panic on i32::MIN, therefore we have a check for it first. + + let width = src.read_i32(); + check_invariant(width != i32::MIN && width.abs() <= 10_000) + .ok_or_else(|| invalid_field_err!("biWidth", "width is too big"))?; + + let height = src.read_i32(); + check_invariant(height != i32::MIN && height.abs() <= 10_000) + .ok_or_else(|| invalid_field_err!("biHeight", "height is too big"))?; + + let planes = src.read_u16(); + if planes != 1 { + return Err(invalid_field_err!("biPlanes", "invalid planes count")); + } + + let bit_count = src.read_u16(); + check_invariant(bit_count <= 32).ok_or_else(|| invalid_field_err!("biBitCount", "invalid bit count"))?; + + let compression = BitmapCompression(src.read_u32()); + let size_image = src.read_u32(); + let x_pels_per_meter = src.read_i32(); + let y_pels_per_meter = src.read_i32(); + let clr_used = src.read_u32(); + let clr_important = src.read_u32(); + + let header = Self { + width, + height, + bit_count, + compression, + size_image, + x_pels_per_meter, + y_pels_per_meter, + clr_used, + clr_important, + }; + + Ok((header, size)) + } + + // INVARIANT: output (width) <= 10_000 + fn width(&self) -> u16 { + let abs = self.width.abs(); + debug_assert!(abs <= 10_000); + u16::try_from(abs).expect("per the invariant on self.width, this cast is infallible") + } + + // INVARIANT: output (height) <= 10_000 + fn height(&self) -> u16 { + let abs = self.height.abs(); + debug_assert!(abs <= 10_000); + u16::try_from(abs).expect("per the invariant on self.height, this cast is infallible") + } + + fn is_bottom_up(&self) -> bool { + // When self.height is positive, the bitmap is defined as bottom-up. + self.height >= 0 + } +} + +impl Encode for BitmapInfoHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let size = cast_int!("biSize", Self::FIXED_PART_SIZE)?; + self.encode_with_size(dst, size) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for BitmapInfoHeader { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + let (header, size) = Self::decode_with_size(src)?; + let size: usize = cast_int!("biSize", size)?; + + if size != Self::FIXED_PART_SIZE { + return Err(invalid_field_err!("biSize", "invalid V1 bitmap info header size")); + } + + Ok(header) + } +} + +/// Header used in `CF_DIBV5` formats, defined as [BITMAPV5HEADER] +/// +/// [BITMAPV5HEADER]: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header +struct BitmapV5Header { + v1: BitmapInfoHeader, + red_mask: u32, + green_mask: u32, + blue_mask: u32, + alpha_mask: u32, + color_space: ColorSpace, + endpoints: CiexyzTriple, + gamma_red: u32, + gamma_green: u32, + gamma_blue: u32, + intent: BitmapIntent, + profile_data: u32, + profile_size: u32, +} + +impl BitmapV5Header { + const FIXED_PART_SIZE: usize = BitmapInfoHeader::FIXED_PART_SIZE // BITMAPV5HEADER + + 4 // bV5RedMask (DWORD) + + 4 // bV5GreenMask (DWORD) + + 4 // bV5BlueMask (DWORD) + + 4 // bV5AlphaMask (DWORD) + + 4 // bV5CSType (DWORD) + + CiexyzTriple::FIXED_PART_SIZE // bV5Endpoints (CIEXYZTRIPLE) + + 4 // bV5GammaRed (DWORD) + + 4 // bV5GammaGreen (DWORD) + + 4 // bV5GammaBlue (DWORD) + + 4 // bV5Intent (DWORD) + + 4 // bV5ProfileData (DWORD) + + 4 // bV5ProfileSize (DWORD) + + 4; // bV5Reserved (DWORD) + + const NAME: &'static str = "BITMAPV5HEADER"; +} + +impl Encode for BitmapV5Header { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let size = cast_int!("biSize", Self::FIXED_PART_SIZE)?; + self.v1.encode_with_size(dst, size)?; + + dst.write_u32(self.red_mask); + dst.write_u32(self.green_mask); + dst.write_u32(self.blue_mask); + dst.write_u32(self.alpha_mask); + dst.write_u32(self.color_space.0); + self.endpoints.encode(dst)?; + dst.write_u32(self.gamma_red); + dst.write_u32(self.gamma_green); + dst.write_u32(self.gamma_blue); + dst.write_u32(self.intent.0); + dst.write_u32(self.profile_data); + dst.write_u32(self.profile_size); + dst.write_u32(0); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for BitmapV5Header { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let (header_v1, size) = BitmapInfoHeader::decode_with_size(src)?; + let size: usize = cast_int!("biSize", size)?; + + if size != Self::FIXED_PART_SIZE { + return Err(invalid_field_err!("biSize", "invalid V5 bitmap info header size")); + } + + let red_mask = src.read_u32(); + let green_mask = src.read_u32(); + let blue_mask = src.read_u32(); + let alpha_mask = src.read_u32(); + let color_space_type = ColorSpace(src.read_u32()); + let endpoints = CiexyzTriple::decode(src)?; + let gamma_red = src.read_u32(); + let gamma_green = src.read_u32(); + let gamma_blue = src.read_u32(); + let intent = BitmapIntent(src.read_u32()); + let profile_data = src.read_u32(); + let profile_size = src.read_u32(); + let _reserved = src.read_u32(); + + Ok(Self { + v1: header_v1, + red_mask, + green_mask, + blue_mask, + alpha_mask, + color_space: color_space_type, + endpoints, + gamma_red, + gamma_green, + gamma_blue, + intent, + profile_data, + profile_size, + }) + } +} + +fn validate_v1_header(header: &BitmapInfoHeader) -> Result<(), BitmapError> { + if header.width < 0 { + return Err(BitmapError::Unsupported("negative width")); + } + + if header.width == 0 || header.height == 0 { + return Err(BitmapError::InvalidSize); + } + + // In the modern world bitmaps with bpp < 24 are rare, and it is even more rare for the bitmaps + // which are placed on the clipboard as DIBs, therefore we could safely skip the support for + // such bitmaps. + const SUPPORTED_BIT_COUNT: &[u16] = &[24, 32]; + + if !SUPPORTED_BIT_COUNT.contains(&header.bit_count) { + return Err(BitmapError::Unsupported("unsupported bit count")); + } + + // This is only relevant for bitmaps with bpp < 24, which are not supported. + if header.clr_used != 0 { + return Err(BitmapError::Unsupported("color table is not supported")); + } + + Ok(()) +} + +fn validate_v5_header(header: &BitmapV5Header) -> Result<(), BitmapError> { + validate_v1_header(&header.v1)?; + + // We support only uncompressed DIB bitmaps as it is the most common case for clipboard-copied bitmaps. + const DIBV5_SUPPORTED_COMPRESSION: &[BitmapCompression] = &[BitmapCompression::RGB, BitmapCompression::BITFIELDS]; + + if !DIBV5_SUPPORTED_COMPRESSION.contains(&header.v1.compression) { + return Err(BitmapError::Unsupported("unsupported compression")); + } + + if header.v1.compression == BitmapCompression::BITFIELDS { + // Currently, we only support the standard order, BGRA, for the bitfields compression. + let is_bgr = header.red_mask == 0x00FF0000 && header.green_mask == 0x0000FF00 && header.blue_mask == 0x000000FF; + + // Note: when there is no alpha channel, the mask is 0x00000000 and we support this too. + let is_supported_alpha = header.alpha_mask == 0 || header.alpha_mask == 0xFF000000; + + if !is_bgr || !is_supported_alpha { + return Err(BitmapError::Unsupported( + "non-standard color masks for `BITFIELDS` compression are not supported", + )); + } + } + + const SUPPORTED_COLOR_SPACE: &[ColorSpace] = &[ + ColorSpace::SRGB, + // Assume that Windows color space is sRGB, either way we don't have enough information on + // the clipboard to convert it to other color spaces. + ColorSpace::WINDOWS, + ]; + + if !SUPPORTED_COLOR_SPACE.contains(&header.color_space) { + return Err(BitmapError::Unsupported("not supported color space")); + } + + Ok(()) +} + +struct PngEncoderContext { + bitmap: Vec, + width: u16, + height: u16, + color_type: png::ColorType, +} + +/// Computes the stride of an uncompressed RGB bitmap. +/// +/// INVARIANT: `width <= output (stride) <= width * 4` +/// +/// In an uncompressed bitmap, the stride is the number of bytes needed to go from the start of one +/// row of pixels to the start of the next row. The image format defines a minimum stride for an +/// image. In addition, the graphics hardware might require a larger stride for the surface that +/// contains the image. +/// +/// For uncompressed RGB formats, the minimum stride is always the image width in bytes, rounded up +/// to the nearest DWORD (4 bytes). The following formula is used to calculate the stride: +/// +/// ``` +/// stride = ((((width * bit_count) + 31) & ~31) >> 3) +/// ``` +/// +/// From Microsoft doc: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader +fn rgb_bmp_stride(width: u16, bit_count: u16) -> usize { + debug_assert!(bit_count <= 32); + + // No side effects, because u16::MAX * 32 + 31 < u16::MAX * u16::MAX < u32::MAX + #[expect(clippy::arithmetic_side_effects)] + { + (((usize::from(width) * usize::from(bit_count)) + 31) & !31) >> 3 + } +} + +fn bgra_to_top_down_rgba( + header: &BitmapInfoHeader, + src_bitmap: &[u8], + preserve_alpha: bool, +) -> Result { + // DIB may be encoded bottom-up, but the format we target, PNG, is top-down. + let should_flip_vertically = header.is_bottom_up(); + + let width = header.width(); + let height = header.height(); + + let src_n_samples = usize::from(header.bit_count / 8); + + let src_stride = rgb_bmp_stride(width, header.bit_count); + + let (dst_color_type, dst_n_samples) = if preserve_alpha { + (png::ColorType::Rgba, 4) + } else { + (png::ColorType::Rgb, 3) + }; + + // Per invariants: height * width * dst_n_samples <= 10_000 * 10_000 * 4 < u32::MAX + #[expect(clippy::arithmetic_side_effects)] + let dst_bitmap_len = usize::from(height) * usize::from(width) * dst_n_samples; + + // Prevent allocation of huge buffers. + ensure(dst_bitmap_len <= MAX_BUFFER_SIZE).ok_or(BitmapError::BufferTooBig)?; + + let mut rows_normal; + let mut rows_reversed; + + let rows: &mut dyn Iterator = if should_flip_vertically { + rows_reversed = src_bitmap.chunks_exact(src_stride).rev(); + &mut rows_reversed + } else { + rows_normal = src_bitmap.chunks_exact(src_stride); + &mut rows_normal + }; + + // DIB stores BGRA colors while PNG uses RGBA. + // DIBv1 (CF_DIB) does not have alpha channel, and the fourth byte is always set to 0xFF. + // DIBv5 (CF_DIBV5) supports alpha channel, so we should preserve it if it is present. + let transform: fn((&mut [u8], &[u8])) = match (header.bit_count, dst_color_type) { + (24 | 32, png::ColorType::Rgb) => |(pixel_out, pixel_in)| { + pixel_out[0] = pixel_in[2]; + pixel_out[1] = pixel_in[1]; + pixel_out[2] = pixel_in[0]; + }, + (24, png::ColorType::Rgba) => |(pixel_out, pixel_in)| { + pixel_out[0] = pixel_in[2]; + pixel_out[1] = pixel_in[1]; + pixel_out[2] = pixel_in[0]; + pixel_out[3] = 0xFF; + }, + (32, png::ColorType::Rgba) => |(pixel_out, pixel_in)| { + pixel_out[0] = pixel_in[2]; + pixel_out[1] = pixel_in[1]; + pixel_out[2] = pixel_in[0]; + pixel_out[3] = pixel_in[3]; + }, + _ => unreachable!("possible values are restricted by header validation and logic above"), + }; + + // Per invariants: width * dst_n_samples <= 10_000 * 4 < u32::MAX + #[expect(clippy::arithmetic_side_effects)] + let dst_stride = usize::from(width) * dst_n_samples; + + let mut dst_bitmap = vec![0u8; dst_bitmap_len]; + + dst_bitmap + .chunks_exact_mut(dst_stride) + .zip(rows) + .for_each(|(dst_row, src_row)| { + let dst_pixels = dst_row.chunks_exact_mut(dst_n_samples); + let src_pixels = src_row.chunks_exact(src_n_samples); + dst_pixels.zip(src_pixels).for_each(transform); + }); + + Ok(PngEncoderContext { + bitmap: dst_bitmap, + width, + height, + color_type: dst_color_type, + }) +} + +fn encode_png(ctx: &PngEncoderContext) -> Result, BitmapError> { + let mut output: Vec = Vec::new(); + + let width = u32::from(ctx.width); + let height = u32::from(ctx.height); + + let mut encoder = png::Encoder::new(&mut output, width, height); + encoder.set_color(ctx.color_type); + encoder.set_depth(png::BitDepth::Eight); + + let mut writer = encoder.write_header()?; + writer.write_image_data(&ctx.bitmap)?; + writer.finish()?; + + Ok(output) +} + +/// Converts `CF_DIB` to PNG. +pub fn dib_to_png(input: &[u8]) -> Result, BitmapError> { + let mut src = ReadCursor::new(input); + let header = BitmapInfoHeader::decode(&mut src).map_err(BitmapError::Decode)?; + + validate_v1_header(&header)?; + + // We support only uncompressed DIB bitmaps as it is the most common case for clipboard-copied bitmaps. + // However, for DIBv1 specifically, BitmapCompression::BITFIELDS is not supported even when the order is BGRA, + // because there is an additional variable-sized header holding the color masks that we don’t support yet. + const DIBV1_SUPPORTED_COMPRESSION: &[BitmapCompression] = &[BitmapCompression::RGB]; + + if !DIBV1_SUPPORTED_COMPRESSION.contains(&header.compression) { + return Err(BitmapError::Unsupported("unsupported compression")); + } + + let png_ctx = bgra_to_top_down_rgba(&header, src.remaining(), false)?; + encode_png(&png_ctx) +} + +/// Converts `CF_DIB` to PNG. +pub fn dibv5_to_png(input: &[u8]) -> Result, BitmapError> { + let mut src = ReadCursor::new(input); + let header = BitmapV5Header::decode(&mut src).map_err(BitmapError::Decode)?; + + validate_v5_header(&header)?; + + let png_ctx = bgra_to_top_down_rgba(&header.v1, src.remaining(), true)?; + encode_png(&png_ctx) +} + +fn top_down_rgba_to_bottom_up_bgra( + info: png::OutputInfo, + src_bitmap: &[u8], +) -> Result<(BitmapInfoHeader, Vec), BitmapError> { + let no_alpha = info.color_type != png::ColorType::Rgba; + let width = u16::try_from(info.width).map_err(|_| BitmapError::WidthTooBig)?; + let height = u16::try_from(info.height).map_err(|_| BitmapError::HeightTooBig)?; + + #[expect(clippy::arithmetic_side_effects)] // width * 4 <= 10_000 * 4 < u32::MAX + let stride = usize::from(width) * 4; + + let src_rows = src_bitmap.chunks_exact(stride); + + // As per invariants: stride * height <= width * 4 * height <= 10_000 * 4 * 10_000 <= u32::MAX. + #[expect(clippy::arithmetic_side_effects)] + let dst_len = stride * usize::from(height); + let dst_len = u32::try_from(dst_len).map_err(|_| BitmapError::InvalidSize)?; + + let header = BitmapInfoHeader { + width: i32::from(width), + height: i32::from(height), + bit_count: 32, // 4 samples * 8 bits + compression: BitmapCompression::RGB, + size_image: dst_len, + x_pels_per_meter: 0, + y_pels_per_meter: 0, + clr_used: 0, + clr_important: 0, + }; + + let dst_len = usize::try_from(dst_len).map_err(|_| BitmapError::InvalidSize)?; + let mut dst_bitmap = vec![0; dst_len]; + + // Reverse rows to draw the image from bottom to top. + let dst_rows = dst_bitmap.chunks_exact_mut(stride).rev(); + + let transform: fn((&mut [u8], &[u8])) = if no_alpha { + |(dst_pixel, src_pixel)| { + dst_pixel[0] = src_pixel[2]; + dst_pixel[1] = src_pixel[1]; + dst_pixel[2] = src_pixel[0]; + dst_pixel[3] = 0xFF; + } + } else { + |(dst_pixel, src_pixel)| { + dst_pixel[0] = src_pixel[2]; + dst_pixel[1] = src_pixel[1]; + dst_pixel[2] = src_pixel[0]; + dst_pixel[3] = src_pixel[3]; + } + }; + + dst_rows.zip(src_rows).for_each(|(dst_row, src_row)| { + let dst_pixels = dst_row.chunks_exact_mut(4); + let src_pixels = src_row.chunks_exact(4); + dst_pixels.zip(src_pixels).for_each(transform); + }); + + Ok((header, dst_bitmap)) +} + +fn decode_png(mut input: &[u8]) -> Result<(png::OutputInfo, Vec), BitmapError> { + let mut decoder = png::Decoder::new(Cursor::new(&mut input)); + + // We need to produce 32-bit DIB, so we should expand the palette to 32-bit RGBA. + decoder.set_transformations(png::Transformations::ALPHA | png::Transformations::EXPAND); + + let mut reader = decoder.read_info()?; + let Some(output_buffer_len) = reader.output_buffer_size() else { + return Err(BitmapError::BufferTooBig); + }; + + // Prevent allocation of huge buffers. + ensure(output_buffer_len <= MAX_BUFFER_SIZE).ok_or(BitmapError::BufferTooBig)?; + + let mut buffer = vec![0; output_buffer_len]; + let info = reader.next_frame(&mut buffer)?; + buffer.truncate(info.buffer_size()); + + Ok((info, buffer)) +} + +/// Converts PNG to `CF_DIB` format. +pub fn png_to_cf_dib(input: &[u8]) -> Result, BitmapError> { + // FIXME(perf): it’s possible to allocate a single array and to directly write both the header and the actual bitmap inside. + // Currently, the code is performing three allocations: one inside `decode_png`, one inside `top_down_rgba_to_bottom_up_bgra` + // and one in the body of this function. + + let (png_info, rgba_bytes) = decode_png(input)?; + let (header, bgra_bytes) = top_down_rgba_to_bottom_up_bgra(png_info, &rgba_bytes)?; + + let output_len = header + .size() + .checked_add(bgra_bytes.len()) + .ok_or(BitmapError::BufferTooBig)?; + + ensure(output_len <= MAX_BUFFER_SIZE).ok_or(BitmapError::BufferTooBig)?; + + let mut output = vec![0; output_len]; + { + let mut dst = WriteCursor::new(&mut output); + header.encode(&mut dst).map_err(BitmapError::Encode)?; + dst.write_slice(&bgra_bytes); + } + + Ok(output) +} + +/// Converts PNG to `CF_DIBV5` format. +pub fn png_to_cf_dibv5(input: &[u8]) -> Result, BitmapError> { + // FIXME(perf): it’s possible to allocate a single array and to directly write both the header and the actual bitmap inside. + // Currently, the code is performing three allocations: one inside `decode_png`, one inside `top_down_rgba_to_bottom_up_bgra` + // and one in the body of this function. + + let (png_info, rgba_bytes) = decode_png(input)?; + let (header_v1, bgra_bytes) = top_down_rgba_to_bottom_up_bgra(png_info, &rgba_bytes)?; + + let header = BitmapV5Header { + v1: header_v1, + // Windows sets these masks for 32-bit bitmaps even if BITFIELDS compression is not used. + red_mask: 0x00FF0000, + green_mask: 0x0000FF00, + blue_mask: 0x000000FF, + alpha_mask: 0xFF000000, + color_space: ColorSpace::SRGB, + endpoints: Default::default(), + gamma_red: 0, + gamma_green: 0, + gamma_blue: 0, + intent: BitmapIntent::LCS_GM_IMAGES, + profile_data: 0, + profile_size: 0, + }; + + let output_len = header + .size() + .checked_add(bgra_bytes.len()) + .ok_or(BitmapError::BufferTooBig)?; + + ensure(output_len <= MAX_BUFFER_SIZE).ok_or(BitmapError::BufferTooBig)?; + + let mut output = vec![0; output_len]; + { + let mut dst = WriteCursor::new(&mut output); + header.encode(&mut dst).map_err(BitmapError::Encode)?; + dst.write_slice(&bgra_bytes); + } + + Ok(output) +} + +/// Use this when establishing invariants. +#[inline] +#[must_use] +fn check_invariant(condition: bool) -> Option<()> { + condition.then_some(()) +} + +/// Returns `None` when the condition is unmet. +#[inline] +#[must_use] +fn ensure(condition: bool) -> Option<()> { + condition.then_some(()) +} diff --git a/crates/ironrdp-cliprdr-format/src/html.rs b/crates/ironrdp-cliprdr-format/src/html.rs new file mode 100644 index 00000000..ed1e2f54 --- /dev/null +++ b/crates/ironrdp-cliprdr-format/src/html.rs @@ -0,0 +1,179 @@ +#[derive(Debug)] +pub enum HtmlError { + InvalidFormat, + InvalidUtf8(core::str::Utf8Error), + InvalidInteger(core::num::ParseIntError), + InvalidConversion, +} + +impl core::fmt::Display for HtmlError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + HtmlError::InvalidFormat => write!(f, "invalid CF_HTML format"), + HtmlError::InvalidUtf8(_error) => write!(f, "invalid UTF-8"), + HtmlError::InvalidInteger(_error) => write!(f, "failed to parse integer"), + HtmlError::InvalidConversion => write!(f, "invalid integer conversion"), + } + } +} + +impl core::error::Error for HtmlError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + HtmlError::InvalidFormat => None, + HtmlError::InvalidUtf8(utf8_error) => Some(utf8_error), + HtmlError::InvalidInteger(parse_int_error) => Some(parse_int_error), + HtmlError::InvalidConversion => None, + } + } +} + +impl From for HtmlError { + fn from(error: core::str::Utf8Error) -> Self { + HtmlError::InvalidUtf8(error) + } +} + +impl From for HtmlError { + fn from(error: core::num::ParseIntError) -> Self { + HtmlError::InvalidInteger(error) + } +} + +/// Converts `CF_HTML` format to plain HTML text. +/// +/// Note that the `CF_HTML` format is using UTF-8, and the input is expected to be valid UTF-8. +/// However, there is no easy way to know the size of the `CF_HTML` payload: +/// 1) it’s typically not null-terminated, and +/// 2) reading the headers is already half of the work. +/// +/// Because of that, this function takes the input as a byte slice and finds the end of the payload itself. +/// This is expected to be more convenient at the callsite. +pub fn cf_html_to_plain_html(input: &[u8]) -> Result<&str, HtmlError> { + const EOL_CONTROL_CHARS: &[u8] = b"\r\n"; + + let mut start_fragment = None; + let mut end_fragment = None; + + // We’ll move the lower bound of this slice until all headers are read. + let mut cursor = input; + + loop { + let line = { + // We use a custom logic for splitting lines, instead of something like `str::lines`. + // That’s because `str::lines` does not split at carriage return (`\r`) not followed by line feed (`\n`). + // In `CF_HTML` format, the line ending could be represented using `\r` alone. + let eol_pos = cursor + .iter() + .position(|byte| EOL_CONTROL_CHARS.contains(byte)) + .ok_or(HtmlError::InvalidFormat)?; + core::str::from_utf8(&cursor[..eol_pos])? + }; + + match line.split_once(':') { + Some((key, value)) => match key { + "StartFragment" => { + start_fragment = Some(header_value_to_u32(value)?); + } + "EndFragment" => { + end_fragment = Some(header_value_to_u32(value)?); + } + _ => { + // We are not interested in other headers. + } + }, + None => { + // At this point, we reached the end of the headers. + if let (Some(start), Some(end)) = (start_fragment, end_fragment) { + let start = usize::try_from(start).map_err(|_| HtmlError::InvalidConversion)?; + let end = usize::try_from(end).map_err(|_| HtmlError::InvalidConversion)?; + + // Ensure start and end values are properly bounded. + if !(start < end && end < input.len()) { + return Err(HtmlError::InvalidFormat); + } + + // Extract the fragment from the original buffer. + let fragment = core::str::from_utf8(&input[start..end])?; + + return Ok(fragment); + } else { + // If required headers were not found, the input is considered invalid. + return Err(HtmlError::InvalidFormat); + } + } + }; + + // Skip EOL control characters and prepare for next line. + cursor = &cursor[line.len()..]; + while let Some(b'\n' | b'\r') = cursor.first() { + cursor = &cursor[1..]; + } + } + + fn header_value_to_u32(value: &str) -> Result { + value.trim_start_matches('0').parse::() + } +} + +/// Converts plain HTML text to `CF_HTML` format. +pub fn plain_html_to_cf_html(fragment: &str) -> String { + const POS_PLACEHOLDER: &str = "0000000000"; + + let mut buffer = String::new(); + + let mut write_header = |key: &str, value: &str| { + // This relation holds: key.len() + value.len() + ":\r\n".len() < usize::MAX + // Rationale: we know all possible values (see code below), and they are much smaller than `usize::MAX`. + #[expect(clippy::arithmetic_side_effects)] + let size = key.len() + value.len() + ":\r\n".len(); + buffer.reserve(size); + + buffer.push_str(key); + buffer.push(':'); + let value_pos = buffer.len(); + buffer.push_str(value); + buffer.push_str("\r\n"); + + value_pos + }; + + write_header("Version", "0.9"); + + let start_html_header_value_pos = write_header("StartHTML", POS_PLACEHOLDER); + let end_html_header_value_pos = write_header("EndHTML", POS_PLACEHOLDER); + let start_fragment_header_value_pos = write_header("StartFragment", POS_PLACEHOLDER); + let end_fragment_header_value_pos = write_header("EndFragment", POS_PLACEHOLDER); + + let start_html_pos = buffer.len(); + buffer.push_str("\r\n\r\n"); + + let start_fragment_pos = buffer.len(); + buffer.push_str(fragment); + + let end_fragment_pos = buffer.len(); + buffer.push_str("\r\n\r\n"); + + let end_html_pos = buffer.len(); + + let start_html_pos_value = format!("{start_html_pos:0>10}"); + let end_html_pos_value = format!("{end_html_pos:0>10}"); + let start_fragment_pos_value = format!("{start_fragment_pos:0>10}"); + let end_fragment_pos_value = format!("{end_fragment_pos:0>10}"); + + let mut replace_placeholder = |value_begin_idx: usize, header_value: &str| { + // We know that: value_begin_idx + POS_PLACEHOLDER.len() < usize::MAX + // Rationale: the headers are written at the beginning, and we’re not indexing outside of the string. + #[expect(clippy::arithmetic_side_effects)] + let value_end_idx = value_begin_idx + POS_PLACEHOLDER.len(); + + buffer.replace_range(value_begin_idx..value_end_idx, header_value); + }; + + replace_placeholder(start_html_header_value_pos, &start_html_pos_value); + replace_placeholder(end_html_header_value_pos, &end_html_pos_value); + replace_placeholder(start_fragment_header_value_pos, &start_fragment_pos_value); + replace_placeholder(end_fragment_header_value_pos, &end_fragment_pos_value); + + buffer +} diff --git a/crates/ironrdp-cliprdr-format/src/lib.rs b/crates/ironrdp-cliprdr-format/src/lib.rs new file mode 100644 index 00000000..1d343605 --- /dev/null +++ b/crates/ironrdp-cliprdr-format/src/lib.rs @@ -0,0 +1,5 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +pub mod bitmap; +pub mod html; diff --git a/crates/ironrdp-cliprdr-native/CHANGELOG.md b/crates/ironrdp-cliprdr-native/CHANGELOG.md new file mode 100644 index 00000000..20bbaa96 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.4.0...ironrdp-cliprdr-native-v0.5.0)] - 2025-12-18 + +### Bug Fixes + +- Prevent window class registration error on multiple sessions ([#1047](https://github.com/Devolutions/IronRDP/issues/1047)) ([a2af587e60](https://github.com/Devolutions/IronRDP/commit/a2af587e60e869f0235703e21772d1fc6a7dadcd)) + + When starting a second clipboard session, `RegisterClassA` would fail + with `ERROR_CLASS_ALREADY_EXISTS` because window classes are global to + the process. Now checks if the class is already registered before + attempting registration, allowing multiple WinClipboard instances to + coexist. + +### Build + +- Bump windows from 0.61.3 to 0.62.1 ([#1010](https://github.com/Devolutions/IronRDP/issues/1010)) ([79e71c4f90](https://github.com/Devolutions/IronRDP/commit/79e71c4f90ea68b14fe45241c1cf3953027b22a2)) + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.3.0...ironrdp-cliprdr-native-v0.4.0)] - 2025-08-29 + +### Bug Fixes + +- Map `E_ACCESSDENIED` WinAPI error code to `ClipboardAccessDenied` error (#936) ([b0c145d0d9](https://github.com/Devolutions/IronRDP/commit/b0c145d0d9cf2f347e537c08ce9d6c35223823d5)) + + When the system clipboard updates, we receive an `Updated` event. Then + we try to open it, but we can get `AccessDenied` error because the + clipboard may still be locked for another window (like _Notepad_). To + handle this, we have special logic that attempts to open the clipboard + in the event of such errors. + The problem is that so far, the `ClipboardAccessDenied` error was not mapped. + +## [[0.1.4](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.1.3...ironrdp-cliprdr-native-v0.1.4)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.1.2...ironrdp-cliprdr-native-v0.1.3)] - 2025-02-03 + +### Bug Fixes + +- Handle `WM_ACTIVATEAPP` in `clipboard_subproc` ([#657](https://github.com/Devolutions/IronRDP/issues/657)) ([9b2926ea12](https://github.com/Devolutions/IronRDP/commit/9b2926ea1212d3f9dec9354334d5bdaa1bebd81e)) + + Previously, the function handled only `WM_ACTIVATE`. + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.1.1...ironrdp-cliprdr-native-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo ([#631](https://github.com/Devolutions/IronRDP/issues/631)) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.1.0...ironrdp-cliprdr-native-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-cliprdr-native/Cargo.toml b/crates/ironrdp-cliprdr-native/Cargo.toml new file mode 100644 index 00000000..be193e66 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "ironrdp-cliprdr-native" +version = "0.5.0" +readme = "README.md" +description = "Native CLIPRDR static channel backend implementations for IronRDP" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.5" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } +tracing = { version = "0.1", features = ["log"] } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_System_DataExchange", + "Win32_System_LibraryLoader", + "Win32_System_Memory", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", +] } + +[lints] +workspace = true diff --git a/crates/ironrdp-cliprdr-native/LICENSE-APACHE b/crates/ironrdp-cliprdr-native/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-cliprdr-native/LICENSE-MIT b/crates/ironrdp-cliprdr-native/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-cliprdr-native/README.md b/crates/ironrdp-cliprdr-native/README.md new file mode 100644 index 00000000..42c396b7 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/README.md @@ -0,0 +1,7 @@ +# IronRDP CLIPRDR native backends + +Native CLIPRDR backend implementations. Currently only Windows is supported. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-cliprdr-native/src/lib.rs b/crates/ironrdp-cliprdr-native/src/lib.rs new file mode 100644 index 00000000..df320550 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/src/lib.rs @@ -0,0 +1,19 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![warn(unsafe_op_in_unsafe_fn)] +#![warn(invalid_reference_casting)] +#![warn(clippy::undocumented_unsafe_blocks)] +#![warn(clippy::multiple_unsafe_ops_per_block)] +#![warn(clippy::transmute_ptr_to_ptr)] +#![warn(clippy::as_ptr_cast_mut)] +#![warn(clippy::cast_ptr_alignment)] +#![warn(clippy::fn_to_numeric_cast_any)] +#![warn(clippy::ptr_cast_constness)] + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use crate::windows::{WinClipboard, WinCliprdrError, WinCliprdrResult, HWND}; + +mod stub; +pub use crate::stub::{StubClipboard, StubCliprdrBackend}; diff --git a/crates/ironrdp-cliprdr-native/src/stub.rs b/crates/ironrdp-cliprdr-native/src/stub.rs new file mode 100644 index 00000000..e6baf6e6 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/src/stub.rs @@ -0,0 +1,99 @@ +use ironrdp_cliprdr::backend::{CliprdrBackend, CliprdrBackendFactory}; +use ironrdp_cliprdr::pdu::{ + ClipboardFormat, ClipboardGeneralCapabilityFlags, FileContentsRequest, FileContentsResponse, FormatDataRequest, + FormatDataResponse, LockDataId, +}; +use ironrdp_core::impl_as_any; +use tracing::debug; + +pub struct StubClipboard; + +impl StubClipboard { + pub fn new() -> Self { + Self + } + + pub fn backend_factory(&self) -> Box { + Box::new(StubCliprdrBackendFactory {}) + } +} + +impl Default for StubClipboard { + fn default() -> Self { + Self::new() + } +} + +struct StubCliprdrBackendFactory {} + +impl CliprdrBackendFactory for StubCliprdrBackendFactory { + fn build_cliprdr_backend(&self) -> Box { + Box::new(StubCliprdrBackend::new()) + } +} + +#[derive(Debug)] +pub struct StubCliprdrBackend; + +impl_as_any!(StubCliprdrBackend); + +impl StubCliprdrBackend { + pub fn new() -> Self { + Self + } +} + +impl Default for StubCliprdrBackend { + fn default() -> Self { + Self::new() + } +} + +impl CliprdrBackend for StubCliprdrBackend { + fn temporary_directory(&self) -> &str { + ".cliprdr" + } + + fn on_ready(&mut self) {} + + fn client_capabilities(&self) -> ClipboardGeneralCapabilityFlags { + // No additional capabilities yet + ClipboardGeneralCapabilityFlags::empty() + } + + fn on_process_negotiated_capabilities(&mut self, capabilities: ClipboardGeneralCapabilityFlags) { + debug!(?capabilities); + } + + fn on_remote_copy(&mut self, available_formats: &[ClipboardFormat]) { + debug!(?available_formats); + } + + fn on_format_data_request(&mut self, request: FormatDataRequest) { + debug!(?request); + } + + fn on_format_data_response(&mut self, response: FormatDataResponse<'_>) { + debug!(?response); + } + + fn on_file_contents_request(&mut self, request: FileContentsRequest) { + debug!(?request); + } + + fn on_file_contents_response(&mut self, response: FileContentsResponse<'_>) { + debug!(?response); + } + + fn on_lock(&mut self, data_id: LockDataId) { + debug!(?data_id); + } + + fn on_unlock(&mut self, data_id: LockDataId) { + debug!(?data_id); + } + + fn on_request_format_list(&mut self) { + debug!("on_request_format_list"); + } +} diff --git a/crates/ironrdp-cliprdr-native/src/windows/clipboard_data_ref.rs b/crates/ironrdp-cliprdr-native/src/windows/clipboard_data_ref.rs new file mode 100644 index 00000000..06672142 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/src/windows/clipboard_data_ref.rs @@ -0,0 +1,74 @@ +use ironrdp_cliprdr::pdu::ClipboardFormatId; +use windows::Win32::Foundation::HGLOBAL; +use windows::Win32::System::DataExchange::GetClipboardData; +use windows::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock}; + +use crate::windows::os_clipboard::OwnedOsClipboard; + +/// Wrapper for global clipboard data handle ready for reading. +pub(crate) struct ClipboardDataRef<'a> { + _os_clipboard: &'a OwnedOsClipboard, + handle: HGLOBAL, + data: *const u8, +} + +impl<'a> ClipboardDataRef<'a> { + /// Get new clipboard data from the clipboard. If no data is available for the specified + /// format, or handle can't be locked, returns `None`. + pub(crate) fn get(os_clipboard: &'a OwnedOsClipboard, format: ClipboardFormatId) -> Option { + // SAFETY: it is safe to call `GetClipboardData`, because we own the clipboard + // before calling this function. + let handle = match unsafe { GetClipboardData(format.value()) } { + Ok(handle) => HGLOBAL(handle.0), + Err(_) => { + // No data available for this format + return None; + } + }; + + // SAFETY: It is safe to call `GlobalLock` on the valid handle. + let data = unsafe { GlobalLock(handle) }.cast::().cast_const(); + + if data.is_null() { + // Can't lock data handle, handle is not valid anymore (e.g. clipboard has changed) + return None; + } + + Some(Self { + _os_clipboard: os_clipboard, + handle, + data, + }) + } + + /// Returns size of the allocated data in bytes. Note that returned size could be larger than + /// actual format data size. For format conversion logic, data buffer should be inspected and + /// its real size should be acquired based on internal format structure. + /// + /// E.g. for `CF_TEXT` + /// format it's required to search for null-terminator to get the actual size of the string. + pub(crate) fn size(&self) -> usize { + // SAFETY: We always own non-null handle, so it is safe to call `GlobalSize` on it + unsafe { GlobalSize(self.handle) } + } + + /// Pointer to the data buffer available for reading. + pub(crate) fn data(&self) -> &[u8] { + let size = self.size(); + // SAFETY: `data` pointer is valid during the lifetime of the wrapper + unsafe { core::slice::from_raw_parts(self.data, size) } + } +} + +impl Drop for ClipboardDataRef<'_> { + fn drop(&mut self) { + // SAFETY: We always own non-null handle, so it is safe to call `GlobalUnlock` on it + if let Err(err) = unsafe { + // Handle with data, retrieved from the clipboard, should be unlocked, but not freed + // (it's owned by the clipboard itself) + GlobalUnlock(self.handle) + } { + tracing::error!("Failed to unlock data: {}", err) + } + } +} diff --git a/crates/ironrdp-cliprdr-native/src/windows/clipboard_impl.rs b/crates/ironrdp-cliprdr-native/src/windows/clipboard_impl.rs new file mode 100644 index 00000000..66632846 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/src/windows/clipboard_impl.rs @@ -0,0 +1,397 @@ +use core::ptr::with_exposed_provenance_mut; +use core::time::Duration; +use std::collections::HashSet; +use std::sync::mpsc; + +use ironrdp_cliprdr::backend::{ClipboardMessage, ClipboardMessageProxy}; +use ironrdp_cliprdr::pdu::{ClipboardFormat, ClipboardFormatId, FormatDataRequest, FormatDataResponse}; +use tracing::warn; +use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; +use windows::Win32::System::DataExchange::GetClipboardOwner; +use windows::Win32::UI::Shell::DefSubclassProc; +use windows::Win32::UI::WindowsAndMessaging::{ + KillTimer, SetTimer, WA_INACTIVE, WM_ACTIVATE, WM_ACTIVATEAPP, WM_CLIPBOARDUPDATE, WM_DESTROY, WM_RENDERALLFORMATS, + WM_RENDERFORMAT, WM_TIMER, +}; + +use crate::windows::clipboard_data_ref::ClipboardDataRef; +use crate::windows::os_clipboard::OwnedOsClipboard; +use crate::windows::remote_format_registry::RemoteClipboardFormatRegistry; +use crate::windows::utils::render_format; +use crate::windows::{BackendEvent, WinCliprdrError, WinCliprdrResult, WM_CLIPRDR_BACKEND_EVENT}; + +const RENDER_FORMAT_TIMEOUT_SECS: u64 = 10; +const IDT_CLIPBOARD_RETRY: usize = 1; + +/// Internal implementation of the clipboard processing logic. +pub(crate) struct WinClipboardImpl { + window: HWND, + message_proxy: Box, + window_is_active: bool, + backend_rx: mpsc::Receiver, + // Number of attempts spent to process current clipboard message + attempt: u32, + // Message to retry + retry_message: Option, + // Formats available on the remote (represented as LOCAL format ids) + available_formats_on_remote: Vec, + remote_format_registry: RemoteClipboardFormatRegistry, +} + +impl WinClipboardImpl { + pub(crate) fn new( + window: HWND, + message_proxy: impl ClipboardMessageProxy + 'static, + backend_rx: mpsc::Receiver, + ) -> Self { + Self { + window, + message_proxy: Box::new(message_proxy), + window_is_active: true, // We assume that we start with current window active, + backend_rx, + attempt: 0, + retry_message: None, + available_formats_on_remote: Vec::new(), + remote_format_registry: Default::default(), + } + } + + fn on_format_data_request(&mut self, request: &FormatDataRequest) -> WinCliprdrResult> { + // Get data from the clipboard and send event to the main event loop + let clipboard = OwnedOsClipboard::new(self.window)?; + + let buffer = ClipboardDataRef::get(&clipboard, request.format).map(|borrowed| borrowed.data().to_vec()); + + let response = match buffer { + Some(data) => FormatDataResponse::new_data(data), + None => { + // No data available for this format + FormatDataResponse::new_error() + } + }; + + Ok(Some(ClipboardMessage::SendFormatData(response))) + } + + fn on_format_data_response( + requested_local_format: ClipboardFormatId, + response: &FormatDataResponse<'_>, + ) -> WinCliprdrResult<()> { + if response.is_error() { + // No data available for this format anymore + return Ok(()); + } + + // SAFETY: `on_format_data_response` is only called in a context of processing + // `WM_RENDERFORMAT` and `WM_RENDERALLFORMATS` messages, so we can safely assume that + // calling `ClipboardData::render` is safe. + unsafe { + render_format(requested_local_format, response.data())?; + } + + Ok(()) + } + + fn on_clipboard_update(&mut self) -> WinCliprdrResult> { + let mut formats = OwnedOsClipboard::new(self.window)?.enum_available_formats()?; + + let mut filtered_format_ids = formats.iter().map(|f| f.id()).collect::>(); + filter_format_ids(&mut filtered_format_ids); + formats.retain(|format| filtered_format_ids.contains(&format.id())); + + Ok(Some(ClipboardMessage::SendInitiateCopy(formats))) + } + + fn get_remote_format_data( + &mut self, + format: ClipboardFormatId, + ) -> WinCliprdrResult>> { + let mapped_format = self.remote_format_registry.local_to_remote(format); + + let remote_format = if let Some(format) = mapped_format { + format + } else { + // Format is unknown or not supported on the local machine + return Ok(None); + }; + + // We need to receive data from the remote clipboard immediately, because Windows + // expects us to set clipboard data before returning from `WM_RENDERFORMAT` handler. + // + // Sadly, this will block the GUI thread, while data is being received + self.message_proxy + .send_clipboard_message(ClipboardMessage::SendInitiatePaste(remote_format)); + let data = self + .backend_rx + .recv_timeout(Duration::from_secs(RENDER_FORMAT_TIMEOUT_SECS)) + .map_err(|_| WinCliprdrError::DataReceiveTimeout)?; + + match data { + BackendEvent::FormatDataResponse(response) => Ok(Some(response)), + _ => { + warn!("Unexpected FormatData response: {:?}", data); + // Unexpected message, ignore it + Ok(None) + } + } + } + + fn on_render_format(&mut self, format: ClipboardFormatId) -> WinCliprdrResult> { + // Owning clipboard is not required when processing `WM_RENDERFORMAT` message + if let Some(response) = self.get_remote_format_data(format)? { + Self::on_format_data_response(format, &response)?; + } + + Ok(None) + } + + fn on_render_all_formats(&mut self) -> WinCliprdrResult> { + // We need to be clipboard owner to be able to set all clipboard formats + let _clipboard = match OwnedOsClipboard::new(self.window) { + Ok(clipboard) => { + // SAFETY: `GetClipboardOwner` is always safe to call + if self.window != unsafe { GetClipboardOwner()? } { + // As per MSDN, we need to validate clipboard owner after opening clipboard + return Ok(None); + } + + clipboard + } + Err(WinCliprdrError::ClipboardAccessDenied) => { + // We don't own the clipboard anymore, we don't need to do anything + return Ok(None); + } + Err(err) => { + return Err(err); + } + }; + + let formats = core::mem::take(&mut self.available_formats_on_remote); + + // Clearing clipboard is not required, just render all available formats + + for format in formats { + if let Some(response) = self.get_remote_format_data(format)? { + Self::on_format_data_response(format, &response)?; + } + } + + Ok(None) + } + + fn on_remote_format_list(&mut self, formats: &[ClipboardFormat]) -> WinCliprdrResult> { + self.available_formats_on_remote.clear(); + + // Clear previous format mapping + self.remote_format_registry.clear(); + + let mut local_format_ids = formats + .iter() + .filter_map(|format| { + // `register` will return None if format is unknown/unsupported on the local machine, + // so we need to filter them out. + self.remote_format_registry.register(format) + }) + .collect::>(); + + filter_format_ids(&mut local_format_ids); + + if local_format_ids.is_empty() { + return Ok(None); + } + + // We need to be clipboard owner to be able to set clipboard data + let mut clipboard = OwnedOsClipboard::new(self.window)?; + // Before sending delayed-rendered data, we need to clear clipboard first. + clipboard.clear()?; + + for format in local_format_ids.into_iter() { + clipboard.delay_render(format)?; + // Store available format for `WM_RENDERALLFORMATS` processing. + self.available_formats_on_remote.push(format); + } + + Ok(None) + } + + fn handle_event(&mut self, event: BackendEvent) { + let result = match &event { + BackendEvent::FormatDataRequest(request) => self.on_format_data_request(request), + BackendEvent::FormatDataResponse(_) => { + // Out-of-order message, ignore it. + Ok(None) + } + BackendEvent::RemoteFormatList(formats) => self.on_remote_format_list(formats), + + BackendEvent::ClipboardUpdated | BackendEvent::RemoteRequestsFormatList => self.on_clipboard_update(), + BackendEvent::RenderFormat(format) => self.on_render_format(*format), + BackendEvent::RenderAllFormats => self.on_render_all_formats(), + + BackendEvent::DowngradedCapabilities(flags) => { + warn!(?flags, "Unhandled downgraded capabilities event"); + Ok(None) + } + }; + + let retry_err = match result { + Ok(Some(message)) => { + self.message_proxy.send_clipboard_message(message); + None + } + Ok(None) => { + // No message to send + None + } + Err(err) => { + // Some errors are retryable (e.g. access to clipboard is temporarily denied) + if let WinCliprdrError::ClipboardAccessDenied = &err { + Some(err) + } else { + self.message_proxy + .send_clipboard_message(ClipboardMessage::Error(Box::new(err))); + None + } + } + }; + + match retry_err { + None => { + self.attempt = 0; + } + Some(err) => { + const MAX_PROCESSING_ATTEMPTS: u32 = 10; + const PROCESSING_TIMEOUT_MS: u32 = 100; + + #[expect(clippy::arithmetic_side_effects)] + // self.attempt can’t be greater than MAX_PROCESSING_ATTEMPTS, so the arithmetic is safe here + if self.attempt < MAX_PROCESSING_ATTEMPTS { + self.attempt += 1; + + self.retry_message = Some(event); + + // SAFETY: `SetTimer` is always safe to call when `hwnd` is a valid window handle + unsafe { + SetTimer( + Some(self.window), + IDT_CLIPBOARD_RETRY, + self.attempt * PROCESSING_TIMEOUT_MS, + None, + ) + }; + } else { + // Send error, retries limit exceeded + self.message_proxy + .send_clipboard_message(ClipboardMessage::Error(Box::new(err))); + } + } + } + } +} + +fn filter_format_ids(formats: &mut HashSet) { + let has_non_utf16_formats = + formats.contains(&ClipboardFormatId::CF_TEXT) || formats.contains(&ClipboardFormatId::CF_OEMTEXT); + + let has_utf16_formats = formats.contains(&ClipboardFormatId::CF_UNICODETEXT); + + // Windows could implicitly convert `CF_UNICODETEXT` to `CF_TEXT`, so to avoid + // character encoding issues with application which prefer non-unicode text, we need to remove + // `CF_TEXT` and `CF_OEMTEXT` from the list of formats. + if has_utf16_formats && has_non_utf16_formats { + formats.remove(&ClipboardFormatId::CF_TEXT); + formats.remove(&ClipboardFormatId::CF_OEMTEXT); + } +} + +/// WinAPI event loop for clipboard processing +/// +/// # Safety +/// +/// This function should only be used for windows subclassing api via `SetWindowSubclass`. +pub(crate) unsafe extern "system" fn clipboard_subproc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _id: usize, + data: usize, +) -> LRESULT { + if msg == WM_DESTROY { + // Transfer ownership and drop previously allocated context + + // SAFETY: `data` is a valid pointer, returned by `Box::into_raw`, transferred to OS earlier + // via `SetWindowSubclass` call. + let _ = unsafe { Box::from_raw(with_exposed_provenance_mut::(data)) }; + return LRESULT(0); + } + + // SAFETY: `data` is a valid pointer, returned by `Box::into_raw`, transferred to OS earlier + // via `SetWindowSubclass` call. + let ctx = unsafe { &mut *(with_exposed_provenance_mut::(data)) }; + + match msg { + // We need to keep track of window state to distinguish between local and remote copy + WM_ACTIVATE | WM_ACTIVATEAPP => { + ctx.window_is_active = wparam.0 != usize::try_from(WA_INACTIVE).expect("WA_INACTIVE fits into usize") + } + // Sent by the OS when OS clipboard content is changed + WM_CLIPBOARDUPDATE => { + // SAFETY: `GetClipboardOwner` is always safe to call. + let clipboard_owner = unsafe { GetClipboardOwner() }; + let spurious_event = clipboard_owner == Ok(hwnd); + + // We need to send copy message from remote only when window is NOT active, because if + // it is active, then user wants to perform copy from remote instead. Also, we need to + // check that we are not the source of the clipboard change, because if we are, + // then we don't need to send copy message to remote. + if !(ctx.window_is_active || spurious_event) { + ctx.handle_event(BackendEvent::ClipboardUpdated); + } + } + // Sent by the OS when delay-rendered data is requested for rendering. + WM_RENDERFORMAT => { + ctx.handle_event(BackendEvent::RenderFormat(ClipboardFormatId::new( + u32::try_from(wparam.0).expect("should never truncate in practice"), + ))); + } + // Sent by the OS when all delay-rendered data is requested for rendering. + WM_RENDERALLFORMATS => { + ctx.handle_event(BackendEvent::RenderAllFormats); + } + // User event, a message was sent by the `Cliprdr` backend shim + WM_CLIPRDR_BACKEND_EVENT => { + let message = if let Ok(message) = ctx.backend_rx.try_recv() { + message + } else { + // No message has been received, spurious event + return LRESULT(0); + }; + + ctx.handle_event(message); + } + // Retry timer. Some operations clipboard operations such as `OpenClipboard` are + // fallible. We need to retry them a few times before giving up. + WM_TIMER => { + if wparam.0 == IDT_CLIPBOARD_RETRY { + // Timer is one-shot, we need to stop it immediately + + // SAFETY: `KillTimer` is always safe to call when `hwnd` is a valid window handle. + if let Err(err) = unsafe { KillTimer(Some(hwnd), IDT_CLIPBOARD_RETRY) } { + tracing::error!("Failed to kill timer: {}", err); + } + + if let Some(event) = ctx.retry_message.take() { + ctx.handle_event(event); + } + } + } + _ => { + // Call next event handler in the subclass chain + + // SAFETY: `DefSubclassProc` is always safe to call in context of subclass event loop + return unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) }; + } + }; + + LRESULT(0) // SUCCESS +} diff --git a/crates/ironrdp-cliprdr-native/src/windows/cliprdr_backend.rs b/crates/ironrdp-cliprdr-native/src/windows/cliprdr_backend.rs new file mode 100644 index 00000000..52c1bf53 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/src/windows/cliprdr_backend.rs @@ -0,0 +1,94 @@ +use std::sync::mpsc as mpsc_sync; + +use ironrdp_cliprdr::backend::CliprdrBackend; +use ironrdp_cliprdr::pdu::{ + ClipboardFormat, ClipboardGeneralCapabilityFlags, FileContentsRequest, FileContentsResponse, FormatDataRequest, + FormatDataResponse, LockDataId, +}; +use ironrdp_core::{impl_as_any, IntoOwned as _}; +use windows::Win32::Foundation::{HWND, LPARAM, WPARAM}; +use windows::Win32::UI::WindowsAndMessaging::PostMessageW; + +use crate::windows::{BackendEvent, WM_CLIPRDR_BACKEND_EVENT}; + +#[derive(Debug)] +pub(crate) struct WinCliprdrBackend { + backend_event_tx: mpsc_sync::SyncSender, + window: HWND, +} + +// SAFETY: window handle is thread safe for PostMessageW usage +unsafe impl Send for WinCliprdrBackend {} + +impl_as_any!(WinCliprdrBackend); + +impl WinCliprdrBackend { + pub(crate) fn new(window: HWND, backend_event_tx: mpsc_sync::SyncSender) -> Self { + Self { + window, + backend_event_tx, + } + } + + fn send_event(&self, event: BackendEvent) { + if self.backend_event_tx.send(event).is_err() { + // Channel is closed, backend is dead + return; + } + // Wake up subproc event loop; Dont wait for result + // + // SAFETY: it is safe to call PostMessageW from any thread with a valid window handle + if let Err(err) = unsafe { PostMessageW(Some(self.window), WM_CLIPRDR_BACKEND_EVENT, WPARAM(0), LPARAM(0)) } { + tracing::error!("Failed to post message to wake up subproc event loop: {}", err); + } + } +} + +impl CliprdrBackend for WinCliprdrBackend { + fn temporary_directory(&self) -> &str { + ".cliprdr" + } + + fn client_capabilities(&self) -> ClipboardGeneralCapabilityFlags { + // No additional capabilities yet + ClipboardGeneralCapabilityFlags::empty() + } + + fn on_ready(&mut self) {} + + fn on_process_negotiated_capabilities(&mut self, capabilities: ClipboardGeneralCapabilityFlags) { + self.send_event(BackendEvent::DowngradedCapabilities(capabilities)) + } + + fn on_remote_copy(&mut self, available_formats: &[ClipboardFormat]) { + self.send_event(BackendEvent::RemoteFormatList(available_formats.to_vec())); + } + + fn on_format_data_request(&mut self, request: FormatDataRequest) { + self.send_event(BackendEvent::FormatDataRequest(request)); + } + + fn on_format_data_response(&mut self, response: FormatDataResponse<'_>) { + self.send_event(BackendEvent::FormatDataResponse(response.into_owned())); + } + + fn on_file_contents_request(&mut self, _request: FileContentsRequest) { + // File transfer not implemented yet + } + + fn on_file_contents_response(&mut self, _response: FileContentsResponse<'_>) { + // File transfer not implemented yet + } + + fn on_lock(&mut self, _data_id: LockDataId) { + // File transfer not implemented yet + } + + fn on_unlock(&mut self, _data_id: LockDataId) { + // File transfer not implemented yet + } + + fn on_request_format_list(&mut self) { + self.send_event(BackendEvent::RemoteRequestsFormatList); + } +} diff --git a/crates/ironrdp-cliprdr-native/src/windows/mod.rs b/crates/ironrdp-cliprdr-native/src/windows/mod.rs new file mode 100644 index 00000000..87574ca5 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/src/windows/mod.rs @@ -0,0 +1,270 @@ +mod clipboard_data_ref; +mod clipboard_impl; +mod cliprdr_backend; +mod os_clipboard; +mod remote_format_registry; +mod utils; + +use std::sync::mpsc as mpsc_sync; + +use ironrdp_cliprdr::backend::{ClipboardMessageProxy, CliprdrBackend, CliprdrBackendFactory}; +use ironrdp_cliprdr::pdu::{ + ClipboardFormat, ClipboardFormatId, ClipboardGeneralCapabilityFlags, FormatDataRequest, FormatDataResponse, +}; +use tracing::error; +use windows::core::{s, Error}; +pub use windows::Win32::Foundation::HWND; +use windows::Win32::Foundation::{E_ACCESSDENIED, FALSE, LPARAM, LRESULT, WPARAM}; +use windows::Win32::System::DataExchange::{AddClipboardFormatListener, RemoveClipboardFormatListener}; +use windows::Win32::System::LibraryLoader::GetModuleHandleA; +use windows::Win32::UI::Shell::{RemoveWindowSubclass, SetWindowSubclass}; +use windows::Win32::UI::WindowsAndMessaging::{ + CreateWindowExA, DefWindowProcA, GetClassInfoA, RegisterClassA, CW_USEDEFAULT, WINDOW_EX_STYLE, WM_USER, WNDCLASSA, + WS_POPUP, +}; + +use self::clipboard_impl::{clipboard_subproc, WinClipboardImpl}; +use self::cliprdr_backend::WinCliprdrBackend; + +const BACKEND_CHANNEL_SIZE: usize = 8; +const WM_CLIPRDR_BACKEND_EVENT: u32 = WM_USER; + +pub type WinCliprdrResult = Result; + +#[derive(Debug)] +pub enum WinCliprdrError { + AddClipboardFormatListener, + FormatsEnumeration, + ClipboardAccessDenied, + ClipboardOpen, + ClipboardEmpty, + Utf16Conversion, + ClipboardData, + SetClipboardData, + WindowSubclass, + Alloc, + DataReceiveTimeout, + RenderFormat, + WinAPI(Error), +} + +impl core::fmt::Display for WinCliprdrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + WinCliprdrError::AddClipboardFormatListener => write!(f, "failed to register clipboard format listener"), + WinCliprdrError::FormatsEnumeration => { + write!(f, "failed to enumerate formats available in the current clipboard") + } + WinCliprdrError::ClipboardAccessDenied => write!(f, "clipboard is busy or denied"), + WinCliprdrError::ClipboardOpen => write!(f, "failed to open the clipboard"), + WinCliprdrError::ClipboardEmpty => write!(f, "failed to empty the clipboard"), + WinCliprdrError::Utf16Conversion => write!(f, "failed to convert UTF-16 string to UTF-8"), + WinCliprdrError::ClipboardData => write!(f, "failed to get current clipboard data"), + WinCliprdrError::SetClipboardData => write!(f, "failed to set clipboard data"), + WinCliprdrError::WindowSubclass => write!(f, "failed to subclass window"), + WinCliprdrError::Alloc => write!(f, "failed to allocate global memory"), + WinCliprdrError::DataReceiveTimeout => write!(f, "failed to receive data from remote clipboard"), + WinCliprdrError::RenderFormat => write!(f, "failed to render clipboard format"), + WinCliprdrError::WinAPI(_error) => write!(f, "WinAPI error"), + } + } +} + +impl core::error::Error for WinCliprdrError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + WinCliprdrError::AddClipboardFormatListener => None, + WinCliprdrError::FormatsEnumeration => None, + WinCliprdrError::ClipboardAccessDenied => None, + WinCliprdrError::ClipboardOpen => None, + WinCliprdrError::ClipboardEmpty => None, + WinCliprdrError::Utf16Conversion => None, + WinCliprdrError::ClipboardData => None, + WinCliprdrError::SetClipboardData => None, + WinCliprdrError::WindowSubclass => None, + WinCliprdrError::Alloc => None, + WinCliprdrError::DataReceiveTimeout => None, + WinCliprdrError::RenderFormat => None, + WinCliprdrError::WinAPI(error) => Some(error), + } + } +} + +impl From for WinCliprdrError { + fn from(err: Error) -> Self { + if err.code() == E_ACCESSDENIED { + WinCliprdrError::ClipboardAccessDenied + } else { + WinCliprdrError::WinAPI(err) + } + } +} + +/// Sent from the clipboard backend shim to the actual WinAPI subproc event loop +#[derive(Debug)] +pub(crate) enum BackendEvent { + // Events generated by OS event loop + ClipboardUpdated, + RenderFormat(ClipboardFormatId), + RenderAllFormats, + + // PDU processing events + DowngradedCapabilities(ClipboardGeneralCapabilityFlags), + RemoteFormatList(Vec), + FormatDataRequest(FormatDataRequest), + FormatDataResponse(FormatDataResponse<'static>), + RemoteRequestsFormatList, +} + +/// Windows RDP client clipboard implementation. +/// +/// IronRDP client implementation should provide raw window handle and message proxy to send +/// messages from the backend to `CLIPRDR` SVC. +/// +/// [`WinClipboard`] instance holds ownership of the actual clipboard backend processing logic +/// and should be kept alive during the whole lifetime of the application. +/// +/// This type is not thread safe, it should be used only in the main thread with a windows event +/// loop. (GUI thread). However, backend factory returned by [`WinClipboard::backend_factory`] +/// can be safely used in other threads. +pub struct WinClipboard { + window: HWND, + backend_tx: mpsc_sync::SyncSender, + + /// From MS docs: + /// ```text + /// You cannot use the subclassing helper functions to subclass a window across threads + /// ``` + /// + /// Therefore this type should be non-Send and non-Sync to prevent incorrect use. + _thread_marker: core::marker::PhantomData<*const ()>, +} + +impl WinClipboard { + /// Creates new clipboard instance. + /// + /// Under the hood, a hidden window is created for capturing the clipboard events. + pub fn new(message_proxy: impl ClipboardMessageProxy + 'static) -> WinCliprdrResult { + extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { + // SAFETY: default handler + unsafe { DefWindowProcA(window, message, wparam, lparam) } + } + + // SAFETY: low-level WinAPI call + let instance = unsafe { GetModuleHandleA(None)? }; + let window_class = s!("IronRDPClipboardMonitor"); + + let mut existing_wc = WNDCLASSA::default(); + // SAFETY: `instance` is a valid module handle, `window_class` is a valid null-terminated string, + // and `existing_wc` is a valid mutable reference to a WNDCLASSA structure. + let class_exists = unsafe { GetClassInfoA(Some(instance.into()), window_class, &mut existing_wc).is_ok() }; + + if !class_exists { + let wc = WNDCLASSA { + hInstance: instance.into(), + lpszClassName: window_class, + lpfnWndProc: Some(wndproc), + ..Default::default() + }; + + // SAFETY: low-level WinAPI call + let atom = unsafe { RegisterClassA(&wc) }; + if atom == 0 { + return Err(WinCliprdrError::from(Error::from_thread())); + } + } + + // SAFETY: low-level WinAPI call + let window = unsafe { + CreateWindowExA( + WINDOW_EX_STYLE::default(), + window_class, + None, + WS_POPUP, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + None, + None, + Some(instance.into()), + None, + )? + }; + + if window.is_invalid() { + return Err(WinCliprdrError::from(Error::from_thread())); + } + // Init clipboard processing for WinAPI event loop + // + // SAFETY: `window` is a valid window handle + unsafe { AddClipboardFormatListener(window)? }; + + let (backend_tx, backend_rx) = mpsc_sync::sync_channel(BACKEND_CHANNEL_SIZE); + + let ctx = Box::new(WinClipboardImpl::new(window, message_proxy, backend_rx)); + + // We need to receive winapi messages in the main thread, so we need to add a subclass to + // the window. + // + // SAFETY: `window` is a valid window handle, `clipboard_subproc` is in the static memory, + // `ctx` is valid and its ownership is transferred to the subclass via `into_raw`. + let winapi_result = unsafe { + SetWindowSubclass( + window, + Some(clipboard_subproc), + 0, + Box::into_raw(ctx).expose_provenance(), + ) + }; + + if winapi_result == FALSE { + return Err(WinCliprdrError::WindowSubclass); + } + + Ok(Self { + window, + backend_tx, + _thread_marker: Default::default(), + }) + } + + /// Returns clipboard backend factory suitable for making backend instances for `CLIPRDR` SVC. + pub fn backend_factory(&self) -> Box { + Box::new(WinCliprdrBackendFactory { + tx: self.backend_tx.clone(), + window: self.window, + }) + } +} + +impl Drop for WinClipboard { + fn drop(&mut self) { + // Remove clipboard processing from WinAPI event loop + + // SAFETY: Format listener was registered in the `new` method previously. + if let Err(err) = unsafe { RemoveClipboardFormatListener(self.window) } { + error!("Failed to remove clipboard listener: {}", err) + } + + // SAFETY: Subclass was registered in the `new` method previously. + if !unsafe { RemoveWindowSubclass(self.window, Some(clipboard_subproc), 0) }.as_bool() { + error!("Failed to remove window subclass") + } + } +} + +/// Windows-specific clipboard backend factory +struct WinCliprdrBackendFactory { + tx: mpsc_sync::SyncSender, + window: HWND, +} + +// SAFETY: window handle is thread safe for PostMessageW usage +unsafe impl Send for WinCliprdrBackendFactory {} + +impl CliprdrBackendFactory for WinCliprdrBackendFactory { + fn build_cliprdr_backend(&self) -> Box { + Box::new(WinCliprdrBackend::new(self.window, self.tx.clone())) + } +} diff --git a/crates/ironrdp-cliprdr-native/src/windows/os_clipboard.rs b/crates/ironrdp-cliprdr-native/src/windows/os_clipboard.rs new file mode 100644 index 00000000..26d34748 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/src/windows/os_clipboard.rs @@ -0,0 +1,113 @@ +use ironrdp_cliprdr::pdu::{ClipboardFormat, ClipboardFormatId, ClipboardFormatName}; +use tracing::error; +use windows::Win32::Foundation::HWND; +use windows::Win32::System::DataExchange::{ + CloseClipboard, EmptyClipboard, EnumClipboardFormats, GetClipboardFormatNameW, OpenClipboard, SetClipboardData, +}; + +use crate::windows::utils::get_last_winapi_error; +use crate::windows::WinCliprdrError; + +/// Safe wrapper around windows. Clipboard is automatically closed on drop. +pub(crate) struct OwnedOsClipboard; + +impl OwnedOsClipboard { + pub(crate) fn new(window: HWND) -> Result { + // SAFETY: `window` is valid handle, therefore it is safe to call `OpenClipboard`. + unsafe { OpenClipboard(Some(window))? }; + Ok(Self) + } + + /// Enumerates all available formats in the current clipboard. + #[expect(clippy::unused_self)] // ensure we own the clipboard using RAII, and exclusive &mut self reference + pub(crate) fn enum_available_formats(&mut self) -> Result, WinCliprdrError> { + const DEFAULT_FORMATS_CAPACITY: usize = 16; + // Sane default for format name. If format name is longer than this, + // `GetClipboardFormatNameW` will truncate it. + const MAX_FORMAT_NAME_LENGTH: usize = 256; + + let mut formats = Vec::with_capacity(DEFAULT_FORMATS_CAPACITY); + + // SAFETY: We own the clipboard at moment of method invocation, therefore it is safe to + // call `EnumClipboardFormats`. + let mut raw_format = unsafe { EnumClipboardFormats(0) }; + let mut format_name_w = [0u16; MAX_FORMAT_NAME_LENGTH]; + + while raw_format != 0 { + let format_id = ClipboardFormatId::new(raw_format); + + // Get format name for predefined formats + let format = if !format_id.is_standard() { + // SAFETY: It is safe to call `GetClipboardFormatNameW` with correct buffer pointer + // and size (wrapped as slice via `windows` crate) + let read_chars: usize = unsafe { GetClipboardFormatNameW(raw_format, &mut format_name_w) } + .try_into() + .expect("never negative"); + + if read_chars != 0 { + let format_name = String::from_utf16(format_name_w[..read_chars].as_ref()) + .map_err(|_| WinCliprdrError::Utf16Conversion)?; + + ClipboardFormat::new(format_id).with_name(ClipboardFormatName::new(format_name)) + } else { + // Unknown format without explicit name + ClipboardFormat::new(format_id) + } + } else { + ClipboardFormat::new(format_id) + }; + + formats.push(format); + + // SAFETY: Same as above, we own the clipboard at moment of method invocation, therefore + // it is safe to call `EnumClipboardFormats`. + raw_format = unsafe { EnumClipboardFormats(raw_format) }; + } + + if get_last_winapi_error().is_err() { + return Err(WinCliprdrError::FormatsEnumeration); + } + + Ok(formats) + } + + /// Empties the clipboard + /// + /// It is required to empty clipboard before setting any delay-rendered data. + #[expect(clippy::unused_self)] // ensure we own the clipboard using RAII, and exclusive &mut self reference + pub(crate) fn clear(&mut self) -> Result<(), WinCliprdrError> { + // SAFETY: We own the clipboard at moment of method invocation, therefore it is safe to + // call `EmptyClipboard`. + unsafe { EmptyClipboard()? }; + + Ok(()) + } + + #[expect(clippy::unused_self)] // ensure we own the clipboard using RAII, and exclusive &mut self reference + pub(crate) fn delay_render(&mut self, format: ClipboardFormatId) -> Result<(), WinCliprdrError> { + // SAFETY: We own the clipboard at moment of method invocation, therefore it is safe to + // call `SetClipboardData`. + let result = unsafe { SetClipboardData(format.value(), None) }; + + if let Err(err) = result { + // `windows` crate will return `Err(..)` on err zero handle, but for `SetClipboardData` + // is is considered as error only if `GetLastError` returns non-zero value + if err.code().is_err() { + error!("Failed to delayed clipboard rendering for format {}", format.value()); + return Err(WinCliprdrError::SetClipboardData); + } + } + + Ok(()) + } +} + +impl Drop for OwnedOsClipboard { + fn drop(&mut self) { + // SAFETY: We own the clipboard at moment of method invocation, therefore it is safe to + // call `CloseClipboard`. + if let Err(err) = unsafe { CloseClipboard() } { + error!("Failed to close clipboard: {}", err); + } + } +} diff --git a/crates/ironrdp-cliprdr-native/src/windows/remote_format_registry.rs b/crates/ironrdp-cliprdr-native/src/windows/remote_format_registry.rs new file mode 100644 index 00000000..5a3b87d4 --- /dev/null +++ b/crates/ironrdp-cliprdr-native/src/windows/remote_format_registry.rs @@ -0,0 +1,113 @@ +use std::collections::HashMap; + +use ironrdp_cliprdr::pdu::{ClipboardFormat, ClipboardFormatId}; +use tracing::error; +use windows::core::PCWSTR; +use windows::Win32::System::DataExchange::RegisterClipboardFormatW; + +use crate::windows::utils::get_last_winapi_error; + +#[derive(Debug, Default)] +pub(crate) struct RemoteClipboardFormatRegistry { + remote_to_local: HashMap, + local_to_remote: HashMap, +} + +impl RemoteClipboardFormatRegistry { + /// Clear current format mapping, as per RDP spec, format mapping is reset on every + /// received `CLIPRDR_FORMAT_LIST` message. + pub(crate) fn clear(&mut self) { + self.remote_to_local.clear(); + self.local_to_remote.clear(); + } + + /// Registers remote clipboard format on local machine. Registered format ids could differ + /// from remote format ids, so we need to keep track of them based on their names. Standard + /// formats such as `CF_TEXT` have fixed ids, which are same on all machines, the returned + /// id value will be same as remote format id. + /// + /// E.g.: Format with name `Custom` was registered as `0xC001`, on the remote, but on the local + /// machine it was registered as `0xC002`. When we receive format list from the remote, we need to + /// get the local format id for `Custom` format, and save format mapping to bi-directional map. + /// When the local machine requests data for `0xC002` format, we will find its mapping for + /// remote format id `0xC001` and send data for it. + /// + /// Returns local format id for the remote format id. + /// If the format is unknown or not supported on the local machine, returns `None`. + pub(crate) fn register(&mut self, remote_format: &ClipboardFormat) -> Option { + if remote_format.id().is_standard() { + // Standard formats such as `CF_TEXT` have fixed ids, which are same on all machines. + return Some(remote_format.id()); + } + + if is_private_format_id(remote_format.id()) { + // Private app-specific formats should not be normally transferred between machines + return None; + } + + if !remote_format.id().is_registered() { + // Unknown format range, we could skip it + return None; + } + + // Try to register format on the local machine + + let format_name = remote_format.name()?; + + // Make null-terminated UTF-16 format representation + let format_name_utf16 = format_name + .value() + .encode_utf16() + .chain(core::iter::once(0)) + .collect::>(); + + let format_name_pcwstr = PCWSTR::from_raw(format_name_utf16.as_ptr()); + + // SAFETY: `RegisterClipboardFormatW` is always safe to call. + let raw_format_id = unsafe { RegisterClipboardFormatW(format_name_pcwstr) }; + + let mapped_format_id = ClipboardFormatId::new(raw_format_id); + + if mapped_format_id.value() == 0 { + let error_code = get_last_winapi_error().0; + error!( + "Failed to register clipboard format `{}`, Error code: {}", + format_name.value(), + error_code + ); + // Error is not critical, format could be skipped + return None; + } + + // save mapping for future use + self.remote_to_local.insert(remote_format.id(), mapped_format_id); + self.local_to_remote.insert(mapped_format_id, remote_format.id()); + + // We either registered new format or found previously registered one + Some(mapped_format_id) + } + + pub(crate) fn local_to_remote(&self, local_format: ClipboardFormatId) -> Option { + if local_format.is_standard() { + return Some(local_format); + } + + self.local_to_remote.get(&local_format).copied() + } +} + +fn is_private_format_id(format: ClipboardFormatId) -> bool { + // Private Windows format ranges which should not be transferred between machines + const CF_PRIVATEFIRST: u32 = 0x0200; + const CF_PRIVATELAST: u32 = 0x02FF; + + const CF_GDIOBJFIRST: u32 = 0x0300; + const CF_GDIOBJLAST: u32 = 0x03FF; + + let id = format.value(); + + let private_range = (CF_PRIVATEFIRST..=CF_PRIVATELAST).contains(&id); + let gdi_range = (CF_GDIOBJFIRST..=CF_GDIOBJLAST).contains(&id); + + private_range || gdi_range +} diff --git a/crates/ironrdp-cliprdr-native/src/windows/utils.rs b/crates/ironrdp-cliprdr-native/src/windows/utils.rs new file mode 100644 index 00000000..198ca81d --- /dev/null +++ b/crates/ironrdp-cliprdr-native/src/windows/utils.rs @@ -0,0 +1,82 @@ +use ironrdp_cliprdr::pdu::ClipboardFormatId; +use tracing::error; +use windows::Win32::Foundation::{GetLastError, GlobalFree, HANDLE, HGLOBAL, WIN32_ERROR}; +use windows::Win32::System::DataExchange::SetClipboardData; +use windows::Win32::System::Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE}; + +use crate::windows::WinCliprdrResult; + +/// Safe wrapper around windows global memory buffer. +struct GlobalMemoryBuffer(HGLOBAL); + +impl GlobalMemoryBuffer { + /// Creates new global memory buffer and copies data into it. + fn from_slice(data: &[u8]) -> WinCliprdrResult { + // SAFETY: GlobalAlloc will return null only if there is not enough memory to allocate + // `windows` crate will catch this error via internal invalid handle check + let handle = unsafe { GlobalAlloc(GMEM_MOVEABLE, data.len())? }; + + // SAFETY: We created the handle and ensured it wasn’t null just above. + // Note that we don’t check for failure because we own the handle and + // know that the specified memory block can’t be discarded at this point. + let dst = unsafe { GlobalLock(handle) }; + + // SAFETY: + // - `data` is valid for reads of `data.len()` bytes. + // - `dst` is valid for writes of `data.len()` bytes, we allocated enough above. + // - Both `data` and `dst` are properly aligned: u8 alignment is 1 + // - Memory regions are not overlapping, `dst` was allocated by us just above. + unsafe { core::ptr::copy_nonoverlapping(data.as_ptr(), dst.cast::(), data.len()) }; + + // SAFETY: We called `GlobalLock` on this handle just above. + if let Err(error) = unsafe { GlobalUnlock(handle) } { + error!(%error, "Failed to unlock memory"); + } + + Ok(Self(handle)) + } + + fn as_raw(&self) -> HGLOBAL { + self.0 + } +} + +impl Drop for GlobalMemoryBuffer { + fn drop(&mut self) { + // SAFETY: It is safe to call GlobalFree on a valid handle + if let Err(err) = unsafe { GlobalFree(Some(self.0)) } { + error!("Failed to free global clipboard data handle: {}", err); + } + } +} + +/// Render data format into the clipboard. +/// +/// # Safety +/// +/// This function should only be called in the context of processing +/// `WM_RENDERFORMAT` or `WM_RENDERALLFORMATS` messages inside WinAPI message loop. +pub(crate) unsafe fn render_format(format: ClipboardFormatId, data: &[u8]) -> WinCliprdrResult<()> { + // Allocate buffer and copy data into it + let global_data = GlobalMemoryBuffer::from_slice(data)?; + + // Cast HGLOBAL to HANDLE + let handle = HANDLE(global_data.as_raw().0); + + // SAFETY: If described above safety requirements of `render_format` call are met, then + // `SetClipboardData` is safe to call. + let _ = unsafe { SetClipboardData(format.value(), Some(handle)) }; + + // We successfully transferred ownership of the data to the clipboard, we don't need to + // call drop on handle + #[expect(clippy::mem_forget)] + core::mem::forget(global_data); + + Ok(()) +} + +/// Return last WinAPI error code. +pub(crate) fn get_last_winapi_error() -> WIN32_ERROR { + // SAFETY: `GetLastError` is always safe to call. + unsafe { GetLastError() } +} diff --git a/crates/ironrdp-cliprdr/CHANGELOG.md b/crates/ironrdp-cliprdr/CHANGELOG.md new file mode 100644 index 00000000..c9653f15 --- /dev/null +++ b/crates/ironrdp-cliprdr/CHANGELOG.md @@ -0,0 +1,74 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-v0.4.0...ironrdp-cliprdr-v0.5.0)] - 2025-12-18 + +### Bug Fixes + +- Fixes the Cliprdr `SvcProcessor` impl to support handling a `TemporaryDirectory` Clipboard PDU ([#1031](https://github.com/Devolutions/IronRDP/issues/1031)) ([f2326ef046](https://github.com/Devolutions/IronRDP/commit/f2326ef046cc81fb0e8985f03382859085882e86)) + +- Allow servers to announce clipboard ownership ([#1053](https://github.com/Devolutions/IronRDP/issues/1053)) ([d587b0c4c1](https://github.com/Devolutions/IronRDP/commit/d587b0c4c114c49d30f52859f43b22f829456a01)) + + Servers can now send Format List PDU via initiate_copy() regardless of + internal state. The existing state machine was designed for clients + where clipboard initialization must complete before announcing + ownership. + + MS-RDPECLIP Section 2.2.3.1 specifies that Format List PDU is sent by + either client or server when the local clipboard is updated. Servers + should be able to announce clipboard changes immediately after channel + negotiation. + + This change enables RDP servers to properly announce clipboard ownership + by bypassing the Initialization/Ready state check when R::is_server() is + true. Client behavior remains unchanged. + +- [**breaking**] Removed the `PackedMetafile::data()` method in favor of making the `PackedMetafile::data` field public. + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-v0.3.0...ironrdp-cliprdr-v0.4.0)] - 2025-08-29 + +### Bug Fixes + +- [**breaking**] Remove the `on_format_list_received` callback (#935) ([5b948e2161](https://github.com/Devolutions/IronRDP/commit/5b948e2161b08b13d32bdbb480b26c8fa44d42f7)) + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-v0.2.0...ironrdp-cliprdr-v0.3.0)] - 2025-05-27 + +### Features + +- [**breaking**] Add on_ready() callback (#729) ([4e581e0f47](https://github.com/Devolutions/IronRDP/commit/4e581e0f47593097c16f2dde43cd0ff0976fe73e)) + + Give a hint to the backend when the channel is actually connected & + ready to process messages. + + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-v0.1.3...ironrdp-cliprdr-v0.2.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-v0.1.2...ironrdp-cliprdr-v0.1.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-v0.1.1...ironrdp-cliprdr-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-v0.1.0...ironrdp-cliprdr-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-cliprdr/Cargo.toml b/crates/ironrdp-cliprdr/Cargo.toml new file mode 100644 index 00000000..3dd216da --- /dev/null +++ b/crates/ironrdp-cliprdr/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ironrdp-cliprdr" +version = "0.5.0" +readme = "README.md" +description = "CLIPRDR static channel for clipboard implemented as described in MS-RDPECLIP" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +tracing = { version = "0.1", features = ["log"] } +bitflags = "2.9" + +[lints] +workspace = true diff --git a/crates/ironrdp-cliprdr/LICENSE-APACHE b/crates/ironrdp-cliprdr/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-cliprdr/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-cliprdr/LICENSE-MIT b/crates/ironrdp-cliprdr/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-cliprdr/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-cliprdr/README.md b/crates/ironrdp-cliprdr/README.md new file mode 100644 index 00000000..ca9b97a3 --- /dev/null +++ b/crates/ironrdp-cliprdr/README.md @@ -0,0 +1,14 @@ +# IronRDP CLIPRDR + +Implementation of clipboard static virtual channel(`CLIPRDR`) described in `MS-RDPECLIP` + +This library includes: +- Clipboard SVC PDUs parsing +- Clipboard SVC processing +- Clipboard backend API types for implementing OS-specific clipboard logic + +For concrete native clipboard backend implementations, see `ironrdp-cliprdr-native` crate. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-cliprdr/src/backend.rs b/crates/ironrdp-cliprdr/src/backend.rs new file mode 100644 index 00000000..7d2bd59c --- /dev/null +++ b/crates/ironrdp-cliprdr/src/backend.rs @@ -0,0 +1,146 @@ +//! This module provides infrastructure for implementing OS-specific clipboard backend. + +use ironrdp_core::AsAny; + +use crate::pdu::{ + ClipboardFormat, ClipboardFormatId, ClipboardGeneralCapabilityFlags, FileContentsRequest, FileContentsResponse, + FormatDataRequest, FormatDataResponse, LockDataId, OwnedFormatDataResponse, +}; + +pub trait ClipboardError: core::error::Error + Send + Sync + 'static {} + +impl ClipboardError for T where T: core::error::Error + Send + Sync + 'static {} + +/// Message sent by the OS clipboard backend event loop. +#[derive(Debug)] +pub enum ClipboardMessage { + /// Sent by clipboard backend when OS clipboard content is changed and ready to be + /// delay-rendered when needed by the remote. + /// + /// Client implementation should initiate copy on `CLIPRDR` SVC when this message is received. + SendInitiateCopy(Vec), + + /// Sent by clipboard backend when format data is ready to be sent to the remote. + /// + /// Client implementation should send format data to `CLIPRDR` SVC when this message is + /// received. + SendFormatData(OwnedFormatDataResponse), + + /// Sent by clipboard backend when format data in given format is need to be received from + /// the remote. + /// + /// Client implementation should send initiate paste on `CLIPRDR` SVC when this message is + /// received. + SendInitiatePaste(ClipboardFormatId), + + /// Failure received from the OS clipboard event loop. + /// + /// Client implementation should log/display this error. + Error(Box), +} + +/// Proxy to send messages from the os clipboard backend to the main application event loop +/// (e.g. winit event loop). +pub trait ClipboardMessageProxy: core::fmt::Debug + Send { + fn send_clipboard_message(&self, message: ClipboardMessage); +} + +/// OS-specific clipboard backend interface. +pub trait CliprdrBackend: AsAny + core::fmt::Debug + Send { + /// Returns path to local temporary directory where clipboard-transferred files should be + /// stored. + fn temporary_directory(&self) -> &str; + + /// Returns capabilities of the client. + /// + /// This method is called by [crate::Cliprdr] when it is + /// ready to send capabilities to the server. Note that this method by itself does not + /// trigger any network activity and values are only used during negotiation phase later. Client + /// should wait for `on_process_negotiated_capabilities` to be called before using any additional + /// [crate::Cliprdr] capabilities. + fn client_capabilities(&self) -> ClipboardGeneralCapabilityFlags; + + /// Called by [crate::Cliprdr] when it is ready to process clipboard data (channel initialized) + fn on_ready(&mut self); + + /// Processes signal to start clipboard copy sequence. + /// + /// Trait implementer is responsible for gathering its list of available [`ClipboardFormat`] + /// and passing them into [`crate::Cliprdr`]'s `initiate_copy` method. + /// + /// Called by [crate::Cliprdr] during initialization phase as a request to start copy + /// sequence on the client. This is needed to advertise available formats on the + /// client's clipboard prior to `CLIPRDR` SVC initialization. + fn on_request_format_list(&mut self); + + /// Adjusts [crate::Cliprdr] backend capabilities based on capabilities negotiated with a server. + /// + /// Called by [crate::Cliprdr] when capability negotiation is finished and server capabilities are + /// received. This method should be used to decide which capabilities should be used by the client. + fn on_process_negotiated_capabilities(&mut self, capabilities: ClipboardGeneralCapabilityFlags); + + /// Processes remote clipboard format list. + /// + /// Called by [crate::Cliprdr] when server sends list of clipboard formats available in remote's + /// clipboard (whenever a cut/copy is executed on remote). + /// + /// Trait implementer should keep track of the latest available formats sent to it through + /// this method. These are needed to be passed in to [`crate::Cliprdr::initiate_paste`] + /// when a user initiates a paste operation of remote data to the local machine. + /// + /// Clipboard endpoint implementation should keep track of available formats prior + /// to requesting data from the server. + fn on_remote_copy(&mut self, available_formats: &[ClipboardFormat]); + + /// Processes remote's request to send format data. + /// + /// Called by [crate::Cliprdr] when server requests data to be copied from the client clipboard. + /// + /// This method only signals the client that server requests data in the given format. + /// Implementors should respond by compiling a [`FormatDataResponse`] and calling + /// [`crate::Cliprdr::submit_format_data`] + fn on_format_data_request(&mut self, request: FormatDataRequest); + + /// Called by [`crate::Cliprdr`] when server sends data to the client clipboard as a response to + /// previously sent format data request. + /// + /// If data is not available anymore, [`FormatDataResponse`] will have its `is_error` field + /// set to `true`. + fn on_format_data_response(&mut self, response: FormatDataResponse<'_>); + + /// Processes remote's request to send file contents. + /// + /// Called by [crate::Cliprdr] when server requests file contents to be copied from the client + /// clipboard. + /// + /// This method only signals the client that server requests specific file contents, and + /// client should respond by calling `submit_file_contents` on [crate::Cliprdr] + fn on_file_contents_request(&mut self, request: FileContentsRequest); + + /// Processes remote's response to previously sent file contents request. + /// + /// Called by [crate::Cliprdr] when server sends file contents to the client clipboard as a response to + /// previously sent file contents request. + /// + /// If data is not available anymore, then server will send error response instead. + fn on_file_contents_response(&mut self, response: FileContentsResponse<'_>); + + /// Locks specific data stream in the client clipboard. + /// + /// Called by [crate::Cliprdr] when server requests to lock client clipboard. + fn on_lock(&mut self, data_id: LockDataId); + + /// Unlocks specific data stream in the client clipboard. + /// + /// Called by [crate::Cliprdr] when server requests to unlock client clipboard. + fn on_unlock(&mut self, data_id: LockDataId); +} + +/// Required to build backend for the OS clipboard implementation. +/// +/// Factory is required because RDP connection could be re-established multiple times, and `CLIPRDR` +/// channel will be re-initialized each time. +pub trait CliprdrBackendFactory { + /// Builds new backend instance. + fn build_cliprdr_backend(&self) -> Box; +} diff --git a/crates/ironrdp-cliprdr/src/lib.rs b/crates/ironrdp-cliprdr/src/lib.rs new file mode 100644 index 00000000..41f5d62c --- /dev/null +++ b/crates/ironrdp-cliprdr/src/lib.rs @@ -0,0 +1,372 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![allow(clippy::arithmetic_side_effects)] // FIXME: remove + +pub mod backend; +pub mod pdu; + +use backend::CliprdrBackend; +use ironrdp_core::{decode, AsAny, EncodeResult}; +use ironrdp_pdu::gcc::ChannelName; +use ironrdp_pdu::{decode_err, encode_err, PduResult}; +use ironrdp_svc::{ + ChannelFlags, CompressionCondition, SvcClientProcessor, SvcMessage, SvcProcessor, SvcProcessorMessages, + SvcServerProcessor, +}; +use pdu::{ + Capabilities, ClientTemporaryDirectory, ClipboardFormat, ClipboardFormatId, ClipboardGeneralCapabilityFlags, + ClipboardPdu, ClipboardProtocolVersion, FileContentsResponse, FormatDataRequest, FormatListResponse, + OwnedFormatDataResponse, +}; +use tracing::{error, info}; + +#[rustfmt::skip] // do not reorder +use crate::pdu::FormatList; + +/// PDUs for sending to the server on the CLIPRDR channel. +pub type CliprdrSvcMessages = SvcProcessorMessages>; + +#[derive(Debug)] +enum ClipboardError { + FormatListRejected, +} + +impl core::fmt::Display for ClipboardError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ClipboardError::FormatListRejected => write!(f, "sent format list was rejected"), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum CliprdrState { + Initialization, + Ready, + Failed, +} + +pub trait Role: core::fmt::Debug + Send + 'static { + fn is_server() -> bool; +} + +/// CLIPRDR static virtual channel endpoint implementation +#[derive(Debug)] +pub struct Cliprdr { + backend: Box, + capabilities: Capabilities, + state: CliprdrState, + _marker: core::marker::PhantomData, +} + +pub type CliprdrClient = Cliprdr; +pub type CliprdrServer = Cliprdr; + +impl SvcClientProcessor for CliprdrClient {} +impl SvcServerProcessor for CliprdrServer {} + +impl AsAny for Cliprdr { + #[inline] + fn as_any(&self) -> &dyn core::any::Any { + self + } + + #[inline] + fn as_any_mut(&mut self) -> &mut dyn core::any::Any { + self + } +} + +macro_rules! ready_guard { + ($self:ident, $function:ident) => {{ + let _ = Self::$function; // ensure the function actually exists + + if $self.state != CliprdrState::Ready { + error!(?$self.state, concat!("Attempted to initiate ", stringify!($function), " in incorrect state")); + return Ok(Vec::new().into()); + } + }}; + } + +impl Cliprdr { + const CHANNEL_NAME: ChannelName = ChannelName::from_static(b"cliprdr\0"); + + pub fn new(backend: Box) -> Self { + // This CLIPRDR implementation supports long format names by default + let flags = ClipboardGeneralCapabilityFlags::USE_LONG_FORMAT_NAMES | backend.client_capabilities(); + + Self { + backend, + state: CliprdrState::Initialization, + capabilities: Capabilities::new(ClipboardProtocolVersion::V2, flags), + _marker: core::marker::PhantomData, + } + } + + pub fn downcast_backend(&self) -> Option<&T> { + self.backend.as_any().downcast_ref::() + } + + pub fn downcast_backend_mut(&mut self) -> Option<&mut T> { + self.backend.as_any_mut().downcast_mut::() + } + + fn are_long_format_names_enabled(&self) -> bool { + self.capabilities + .flags() + .contains(ClipboardGeneralCapabilityFlags::USE_LONG_FORMAT_NAMES) + } + + fn build_format_list(&self, formats: &[ClipboardFormat]) -> EncodeResult> { + FormatList::new_unicode(formats, self.are_long_format_names_enabled()) + } + + fn handle_error_transition(&mut self, err: ClipboardError) -> PduResult> { + // Failure of clipboard is not an critical error, but we should properly report it + // and transition channel to failed state. + self.state = CliprdrState::Failed; + error!("CLIPRDR(clipboard) failed: {err}"); + + Ok(Vec::new()) + } + + fn handle_server_capabilities(&mut self, server_capabilities: Capabilities) -> PduResult> { + self.capabilities.downgrade(&server_capabilities); + self.backend + .on_process_negotiated_capabilities(self.capabilities.flags()); + + // Do not send anything, wait for monitor ready pdu + Ok(Vec::new()) + } + + fn handle_monitor_ready(&mut self) -> PduResult> { + // Request client to sent list of initially available formats and wait for the backend + // response. + self.backend.on_request_format_list(); + Ok(Vec::new()) + } + + fn handle_format_list_response(&mut self, response: FormatListResponse) -> PduResult> { + match response { + FormatListResponse::Ok => { + if !R::is_server() { + if self.state == CliprdrState::Initialization { + info!("CLIPRDR(clipboard) virtual channel has been initialized"); + self.state = CliprdrState::Ready; + self.backend.on_ready(); + } else { + info!("CLIPRDR(clipboard) Remote has received format list successfully"); + } + } + } + FormatListResponse::Fail => { + return self.handle_error_transition(ClipboardError::FormatListRejected); + } + } + + Ok(Vec::new()) + } + + fn handle_format_list(&mut self, format_list: FormatList<'_>) -> PduResult> { + if R::is_server() && self.state == CliprdrState::Initialization { + info!("CLIPRDR(clipboard) virtual channel has been initialized"); + self.state = CliprdrState::Ready; + self.backend.on_ready(); + } + + let formats = format_list.get_formats(self.are_long_format_names_enabled())?; + self.backend.on_remote_copy(&formats); + + let pdu = ClipboardPdu::FormatListResponse(FormatListResponse::Ok); + + Ok(vec![into_cliprdr_message(pdu)]) + } + + /// Submits the format data response, returning a [`CliprdrSvcMessages`] to send on the channel. + /// + /// Should be called by the clipboard implementation when it receives data from the OS clipboard + /// and is ready to sent it to the server. This should happen after + /// [`CliprdrBackend::on_format_data_request`] is called by [`Cliprdr`]. + /// + /// If data is not available anymore, an error response should be sent instead. + pub fn submit_format_data(&self, response: OwnedFormatDataResponse) -> PduResult> { + ready_guard!(self, submit_format_data); + + let pdu = ClipboardPdu::FormatDataResponse(response); + + Ok(vec![into_cliprdr_message(pdu)].into()) + } + + /// Submits the file contents response, returning a [`CliprdrSvcMessages`] to send on the channel. + /// + /// Should be called by the clipboard implementation when file data is ready to sent it to the + /// server. This should happen after [`CliprdrBackend::on_file_contents_request`] is called + /// by [`Cliprdr`]. + /// + /// If data is not available anymore, an error response should be sent instead. + pub fn submit_file_contents(&self, response: FileContentsResponse<'static>) -> PduResult> { + ready_guard!(self, submit_file_contents); + + let pdu = ClipboardPdu::FileContentsResponse(response); + + Ok(vec![into_cliprdr_message(pdu)].into()) + } + + pub fn capabilities(&self) -> PduResult { + let pdu = ClipboardPdu::Capabilities(self.capabilities.clone()); + + Ok(into_cliprdr_message(pdu)) + } + + pub fn monitor_ready(&self) -> PduResult { + let pdu = ClipboardPdu::MonitorReady; + + Ok(into_cliprdr_message(pdu)) + } + + /// Starts processing of `CLIPRDR` copy command. Should be called by the clipboard + /// implementation when user performs OS-specific copy command (e.g. `Ctrl+C` shortcut on + /// keyboard) + pub fn initiate_copy(&self, available_formats: &[ClipboardFormat]) -> PduResult> { + let mut pdus = Vec::new(); + + if R::is_server() { + pdus.push(ClipboardPdu::FormatList( + self.build_format_list(available_formats).map_err(|e| encode_err!(e))?, + )); + } else { + match self.state { + CliprdrState::Ready => { + pdus.push(ClipboardPdu::FormatList( + self.build_format_list(available_formats).map_err(|e| encode_err!(e))?, + )); + } + CliprdrState::Initialization => { + // During initialization state, first copy action is synthetic and should be sent along with + // capabilities and temporary directory PDUs. + pdus.push(ClipboardPdu::Capabilities(self.capabilities.clone())); + pdus.push(ClipboardPdu::TemporaryDirectory( + ClientTemporaryDirectory::new(self.backend.temporary_directory()) + .map_err(|e| encode_err!(e))?, + )); + pdus.push(ClipboardPdu::FormatList( + self.build_format_list(available_formats).map_err(|e| encode_err!(e))?, + )); + } + _ => { + error!(?self.state, "Attempted to initiate copy in incorrect state"); + } + } + } + + Ok(pdus.into_iter().map(into_cliprdr_message).collect::>().into()) + } + + /// Starts processing of `CLIPRDR` paste command. Should be called by the clipboard + /// implementation when user performs OS-specific paste command (e.g. `Ctrl+V` shortcut on + /// keyboard) + pub fn initiate_paste(&self, requested_format: ClipboardFormatId) -> PduResult> { + ready_guard!(self, initiate_paste); + + // When user initiates paste, we should send format data request to server, and expect to + // receive response with contents via `FormatDataResponse` PDU. + let pdu = ClipboardPdu::FormatDataRequest(FormatDataRequest { + format: requested_format, + }); + + Ok(vec![into_cliprdr_message(pdu)].into()) + } +} + +impl SvcProcessor for Cliprdr { + fn channel_name(&self) -> ChannelName { + Self::CHANNEL_NAME + } + + fn start(&mut self) -> PduResult> { + if self.state != CliprdrState::Initialization { + error!("Attempted to start clipboard static virtual channel in invalid state"); + } + + if R::is_server() { + Ok(vec![self.capabilities()?, self.monitor_ready()?]) + } else { + Ok(Vec::new()) + } + } + + fn process(&mut self, payload: &[u8]) -> PduResult> { + let pdu = decode::>(payload).map_err(|e| decode_err!(e))?; + + if self.state == CliprdrState::Failed { + error!("Attempted to process clipboard static virtual channel in failed state"); + return Ok(Vec::new()); + } + + match pdu { + ClipboardPdu::Capabilities(caps) => self.handle_server_capabilities(caps), + ClipboardPdu::FormatList(format_list) => self.handle_format_list(format_list), + ClipboardPdu::FormatListResponse(response) => self.handle_format_list_response(response), + ClipboardPdu::MonitorReady => self.handle_monitor_ready(), + ClipboardPdu::LockData(id) => { + self.backend.on_lock(id); + Ok(Vec::new()) + } + ClipboardPdu::UnlockData(id) => { + self.backend.on_unlock(id); + Ok(Vec::new()) + } + ClipboardPdu::FormatDataRequest(request) => { + self.backend.on_format_data_request(request); + + // NOTE: An actual data should be sent later via `submit_format_data` method, + // therefore we do not send anything immediately. + Ok(Vec::new()) + } + ClipboardPdu::FormatDataResponse(response) => { + self.backend.on_format_data_response(response); + Ok(Vec::new()) + } + ClipboardPdu::FileContentsRequest(request) => { + self.backend.on_file_contents_request(request); + Ok(Vec::new()) + } + ClipboardPdu::FileContentsResponse(response) => { + self.backend.on_file_contents_response(response); + Ok(Vec::new()) + } + ClipboardPdu::TemporaryDirectory(_) => { + // do nothing + Ok(Vec::new()) + } + } + } + + fn compression_condition(&self) -> CompressionCondition { + CompressionCondition::WhenRdpDataIsCompressed + } +} + +fn into_cliprdr_message(pdu: ClipboardPdu<'static>) -> SvcMessage { + // Adding [`CHANNEL_FLAG_SHOW_PROTOCOL`] is a must for clipboard svc messages, because they + // contain chunked data. This is the requirement from `MS_RDPBCGR` specification. + SvcMessage::from(pdu).with_flags(ChannelFlags::SHOW_PROTOCOL) +} + +#[derive(Debug)] +pub struct Client {} + +impl Role for Client { + fn is_server() -> bool { + false + } +} + +#[derive(Debug)] +pub struct Server {} + +impl Role for Server { + fn is_server() -> bool { + true + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/capabilities.rs b/crates/ironrdp-cliprdr/src/pdu/capabilities.rs new file mode 100644 index 00000000..377586ea --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/capabilities.rs @@ -0,0 +1,293 @@ +use bitflags::bitflags; +use ironrdp_core::{ + cast_int, cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeError, DecodeResult, + Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use ironrdp_pdu::{impl_pdu_pod, read_padding, write_padding}; + +use crate::pdu::PartialHeader; + +/// Represents `CLIPRDR_CAPS` +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct Capabilities { + pub capabilities: Vec, +} + +impl_pdu_pod!(Capabilities); + +impl Capabilities { + const NAME: &'static str = "CLIPRDR_CAPS"; + const FIXED_PART_SIZE: usize = 2 /* capsLen */ + 2 /* padding */; + + fn inner_size(&self) -> usize { + Self::FIXED_PART_SIZE + self.capabilities.iter().map(|c| c.size()).sum::() + } + + pub fn new(version: ClipboardProtocolVersion, general_flags: ClipboardGeneralCapabilityFlags) -> Self { + let capabilities = vec![CapabilitySet::General(GeneralCapabilitySet { version, general_flags })]; + + Self { capabilities } + } + + pub fn flags(&self) -> ClipboardGeneralCapabilityFlags { + // There is only one capability set in the capabilities field in current CLIPRDR version + self.capabilities + .first() + .map(|set| set.general().general_flags) + .unwrap_or_else(ClipboardGeneralCapabilityFlags::empty) + } + + pub fn version(&self) -> ClipboardProtocolVersion { + self.capabilities + .first() + .map(|set| set.general().version) + .unwrap_or(ClipboardProtocolVersion::V1) + } + + pub fn downgrade(&mut self, server_caps: &Self) { + let client_flags = self.flags(); + let server_flags = self.flags(); + + let flags = client_flags & server_flags; + let version = self.version().downgrade(server_caps.version()); + + self.capabilities = vec![CapabilitySet::General(GeneralCapabilitySet { + version, + general_flags: flags, + })]; + } +} + +impl Encode for Capabilities { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", self.inner_size())?); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.inner_size()); + + dst.write_u16(cast_length!(Self::NAME, "cCapabilitiesSets", self.capabilities.len())?); + write_padding!(dst, 2); + + for capability in &self.capabilities { + capability.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.inner_size() + PartialHeader::SIZE + } +} + +impl<'de> Decode<'de> for Capabilities { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let _header = PartialHeader::decode(src)?; + + ensure_fixed_part_size!(in: src); + let capabilities_count = src.read_u16(); + read_padding!(src, 2); + + let mut capabilities = Vec::with_capacity(usize::from(capabilities_count)); + + for _ in 0..capabilities_count { + let caps = CapabilitySet::decode(src)?; + capabilities.push(caps); + } + + Ok(Self { capabilities }) + } +} + +/// Represents `CLIPRDR_CAPS_SET` +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CapabilitySet { + General(GeneralCapabilitySet), +} + +impl_pdu_pod!(CapabilitySet); + +impl CapabilitySet { + const NAME: &'static str = "CLIPRDR_CAPS_SET"; + const FIXED_PART_SIZE: usize = 2 /* type */ + 2 /* len */; + + const CAPSTYPE_GENERAL: u16 = 0x0001; + + pub fn general(&self) -> &GeneralCapabilitySet { + match self { + Self::General(value) => value, + } + } +} + +impl From for CapabilitySet { + fn from(value: GeneralCapabilitySet) -> Self { + Self::General(value) + } +} + +impl Encode for CapabilitySet { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let (caps, length) = match self { + Self::General(value) => { + let length = value.size() + Self::FIXED_PART_SIZE; + (value, length) + } + }; + + ensure_size!(in: dst, size: length); + dst.write_u16(Self::CAPSTYPE_GENERAL); + dst.write_u16(cast_int!("lengthCapability", length)?); + caps.encode(dst) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let variable_size = match self { + Self::General(value) => value.size(), + }; + + Self::FIXED_PART_SIZE + variable_size + } +} + +impl<'de> Decode<'de> for CapabilitySet { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let caps_type = src.read_u16(); + let _length = src.read_u16(); + + match caps_type { + Self::CAPSTYPE_GENERAL => { + let general = GeneralCapabilitySet::decode(src)?; + Ok(Self::General(general)) + } + _ => Err(invalid_field_err!( + "capabilitySetType", + "invalid clipboard capability set type" + )), + } + } +} + +/// Represents `CLIPRDR_GENERAL_CAPABILITY` without header +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneralCapabilitySet { + pub version: ClipboardProtocolVersion, + pub general_flags: ClipboardGeneralCapabilityFlags, +} + +impl GeneralCapabilitySet { + const NAME: &'static str = "CLIPRDR_GENERAL_CAPABILITY"; + const FIXED_PART_SIZE: usize = 4 /* version */ + 4 /* flags */; +} + +impl Encode for GeneralCapabilitySet { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.version.into()); + dst.write_u32(self.general_flags.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for GeneralCapabilitySet { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version: ClipboardProtocolVersion = src.read_u32().try_into()?; + let general_flags = ClipboardGeneralCapabilityFlags::from_bits_truncate(src.read_u32()); + + Ok(Self { version, general_flags }) + } +} + +/// Specifies the `Remote Desktop Protocol: Clipboard Virtual Channel Extension` version number. +/// This field is for informational purposes and MUST NOT be used to make protocol capability +/// decisions. The actual features supported are specified via [`ClipboardGeneralCapabilityFlags`] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ClipboardProtocolVersion { + V1, + V2, +} + +impl ClipboardProtocolVersion { + const VERSION_VALUE_V1: u32 = 0x0000_0001; + const VERSION_VALUE_V2: u32 = 0x0000_0002; + + #[must_use] + pub fn downgrade(self, other: Self) -> Self { + if self != other { + return Self::V1; + } + self + } +} + +impl From for u32 { + fn from(version: ClipboardProtocolVersion) -> Self { + match version { + ClipboardProtocolVersion::V1 => ClipboardProtocolVersion::VERSION_VALUE_V1, + ClipboardProtocolVersion::V2 => ClipboardProtocolVersion::VERSION_VALUE_V2, + } + } +} + +impl TryFrom for ClipboardProtocolVersion { + type Error = DecodeError; + + fn try_from(value: u32) -> Result { + match value { + Self::VERSION_VALUE_V1 => Ok(Self::V1), + Self::VERSION_VALUE_V2 => Ok(Self::V2), + _ => Err(invalid_field_err!("version", "Invalid clipboard capabilities version")), + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ClipboardGeneralCapabilityFlags: u32 { + /// The Long Format Name variant of the Format List PDU is supported + /// for exchanging updated format names. If this flag is not set, the + /// Short Format Name variant MUST be used. If this flag is set by both + /// protocol endpoints, then the Long Format Name variant MUST be + /// used. + const USE_LONG_FORMAT_NAMES = 0x0000_0002; + /// File copy and paste using stream-based operations are supported + /// using the File Contents Request PDU and File Contents Response + /// PDU. + const STREAM_FILECLIP_ENABLED = 0x0000_0004; + /// Indicates that any description of files to copy and paste MUST NOT + /// include the source path of the files. + const FILECLIP_NO_FILE_PATHS = 0x0000_0008; + /// Locking and unlocking of File Stream data on the clipboard is + /// supported using the Lock Clipboard Data PDU and Unlock Clipboard + /// Data PDU. + const CAN_LOCK_CLIPDATA = 0x0000_0010; + /// Indicates support for transferring files that are larger than + /// 4,294,967,295 bytes in size. If this flag is not set, then only files of + /// size less than or equal to 4,294,967,295 bytes can be exchanged + /// using the File Contents Request PDU and File Contents + /// Response PDU. + const HUGE_FILE_SUPPORT_ENABLED = 0x0000_0020; + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/client_temporary_directory.rs b/crates/ironrdp-cliprdr/src/pdu/client_temporary_directory.rs new file mode 100644 index 00000000..53e778ae --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/client_temporary_directory.rs @@ -0,0 +1,90 @@ +use std::borrow::Cow; + +use ironrdp_core::{ + cast_int, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, + WriteCursor, +}; +use ironrdp_pdu::impl_pdu_borrowing; +use ironrdp_pdu::utils::{read_string_from_cursor, write_string_to_cursor, CharacterSet}; + +use crate::pdu::PartialHeader; + +/// Represents `CLIPRDR_TEMP_DIRECTORY` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientTemporaryDirectory<'a> { + path_buffer: Cow<'a, [u8]>, +} + +impl_pdu_borrowing!(ClientTemporaryDirectory<'_>, OwnedClientTemporaryDirectory); + +impl IntoOwned for ClientTemporaryDirectory<'_> { + type Owned = OwnedClientTemporaryDirectory; + + fn into_owned(self) -> Self::Owned { + OwnedClientTemporaryDirectory { + path_buffer: Cow::Owned(self.path_buffer.into_owned()), + } + } +} + +impl ClientTemporaryDirectory<'_> { + const PATH_BUFFER_SIZE: usize = 520; + + const NAME: &'static str = "CLIPRDR_TEMP_DIRECTORY"; + const INNER_SIZE: usize = Self::PATH_BUFFER_SIZE; + + /// Creates new `ClientTemporaryDirectory` and encodes given path to UTF-16 representation. + pub fn new(path: &str) -> EncodeResult { + let mut buffer = vec![0x00; Self::PATH_BUFFER_SIZE]; + + { + let mut cursor = WriteCursor::new(&mut buffer); + write_string_to_cursor(&mut cursor, path, CharacterSet::Unicode, true)?; + } + + Ok(Self { + path_buffer: Cow::Owned(buffer), + }) + } + + /// Returns parsed temporary directory path. + pub fn temporary_directory_path(&self) -> DecodeResult { + let mut cursor = ReadCursor::new(&self.path_buffer); + + read_string_from_cursor(&mut cursor, CharacterSet::Unicode, true) + .map_err(|_| invalid_field_err!("wszTempDir", "failed to decode temp dir path")) + } +} + +impl Encode for ClientTemporaryDirectory<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", Self::INNER_SIZE)?); + header.encode(dst)?; + + ensure_size!(in: dst, size: Self::INNER_SIZE); + dst.write_slice(&self.path_buffer); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + Self::INNER_SIZE + } +} + +impl<'de> Decode<'de> for ClientTemporaryDirectory<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let _header = PartialHeader::decode(src)?; + + ensure_size!(in: src, size: Self::INNER_SIZE); + let buffer = src.read_slice(Self::PATH_BUFFER_SIZE); + + Ok(Self { + path_buffer: Cow::Borrowed(buffer), + }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/file_contents.rs b/crates/ironrdp-cliprdr/src/pdu/file_contents.rs new file mode 100644 index 00000000..1651b1e1 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/file_contents.rs @@ -0,0 +1,248 @@ +use std::borrow::Cow; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_int, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, + WriteCursor, +}; +use ironrdp_pdu::impl_pdu_borrowing; +use ironrdp_pdu::utils::{combine_u64, split_u64}; + +use crate::pdu::{ClipboardPduFlags, PartialHeader}; + +bitflags! { + /// Represents `dwFlags` field of `CLIPRDR_FILECONTENTS_REQUEST` structure. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct FileContentsFlags: u32 { + /// A request for the size of the file identified by the lindex field. The size MUST be + /// returned as a 64-bit, unsigned integer. The cbRequested field MUST be set to + /// 0x00000008 and both the nPositionLow and nPositionHigh fields MUST be + /// set to 0x00000000. + const SIZE = 0x0000_0001; + /// A request for the data present in the file identified by the lindex field. The data + /// to be retrieved is extracted starting from the offset given by the nPositionLow + /// and nPositionHigh fields. The maximum number of bytes to extract is specified + /// by the cbRequested field. + const DATA = 0x0000_0002; + } +} + +/// Represents `CLIPRDR_FILECONTENTS_RESPONSE` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileContentsResponse<'a> { + is_error: bool, + stream_id: u32, + data: Cow<'a, [u8]>, +} + +impl_pdu_borrowing!(FileContentsResponse<'_>, OwnedFileContentsResponse); + +impl IntoOwned for FileContentsResponse<'_> { + type Owned = OwnedFileContentsResponse; + + fn into_owned(self) -> Self::Owned { + OwnedFileContentsResponse { + is_error: self.is_error, + stream_id: self.stream_id, + data: Cow::Owned(self.data.into_owned()), + } + } +} + +impl<'a> FileContentsResponse<'a> { + const NAME: &'static str = "CLIPRDR_FILECONTENTS_RESPONSE"; + const FIXED_PART_SIZE: usize = 4 /* streamId */; + + fn inner_size(&self) -> usize { + Self::FIXED_PART_SIZE + self.data.len() + } + + /// Creates a new `FileContentsResponse` with u64 size value + pub fn new_size_response(stream_id: u32, size: u64) -> Self { + Self { + is_error: false, + stream_id, + data: Cow::Owned(size.to_le_bytes().to_vec()), + } + } + + /// Creates a new `FileContentsResponse` with file contents value + pub fn new_data_response(stream_id: u32, data: impl Into>) -> Self { + Self { + is_error: false, + stream_id, + data: data.into(), + } + } + + /// Creates new `FileContentsResponse` with error + pub fn new_error(stream_id: u32) -> Self { + Self { + is_error: true, + stream_id, + data: Cow::Borrowed(&[]), + } + } + + pub fn stream_id(&self) -> u32 { + self.stream_id + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + /// Read data as u64 size value + pub fn data_as_size(&self) -> DecodeResult { + let chunk = self + .data + .as_ref() + .try_into() + .map_err(|_| invalid_field_err!("requestedFileContentsData", "not enough bytes for u64 size"))?; + + Ok(u64::from_le_bytes(chunk)) + } +} + +impl Encode for FileContentsResponse<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let flags = if self.is_error { + ClipboardPduFlags::RESPONSE_FAIL + } else { + ClipboardPduFlags::RESPONSE_OK + }; + + let header = PartialHeader::new_with_flags(cast_int!("dataLen", self.inner_size())?, flags); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.inner_size()); + + dst.write_u32(self.stream_id); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + self.inner_size() + } +} + +impl<'de> Decode<'de> for FileContentsResponse<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let header = PartialHeader::decode(src)?; + + let is_error = header.message_flags.contains(ClipboardPduFlags::RESPONSE_FAIL); + + ensure_size!(in: src, size: header.data_length()); + + if header.data_length() < Self::FIXED_PART_SIZE { + return Err(invalid_field_err!("requestedFileContentsData", "Invalid data size")); + }; + + let data_size = header.data_length() - Self::FIXED_PART_SIZE; + + let stream_id = src.read_u32(); + let data = src.read_slice(data_size); + + Ok(Self { + is_error, + stream_id, + data: Cow::Borrowed(data), + }) + } +} + +/// Represents `CLIPRDR_FILECONTENTS_REQUEST` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileContentsRequest { + pub stream_id: u32, + pub index: u32, + pub flags: FileContentsFlags, + pub position: u64, + pub requested_size: u32, + pub data_id: Option, +} + +impl FileContentsRequest { + const NAME: &'static str = "CLIPRDR_FILECONTENTS_REQUEST"; + const FIXED_PART_SIZE: usize = 4 /* streamId */ + 4 /* idx */ + 4 /* flags */ + 8 /* position */ + 4 /* reqSize */; + + fn inner_size(&self) -> usize { + let data_id_size = match self.data_id { + Some(_) => 4, + None => 0, + }; + + Self::FIXED_PART_SIZE + data_id_size + } +} + +impl Encode for FileContentsRequest { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", self.inner_size())?); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.inner_size()); + + dst.write_u32(self.stream_id); + dst.write_u32(self.index); + dst.write_u32(self.flags.bits()); + + let (position_lo, position_hi) = split_u64(self.position); + dst.write_u32(position_lo); + dst.write_u32(position_hi); + dst.write_u32(self.requested_size); + + if let Some(data_id) = self.data_id { + dst.write_u32(data_id); + }; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + self.inner_size() + } +} + +impl<'de> Decode<'de> for FileContentsRequest { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let header = PartialHeader::decode(src)?; + + let read_data_id = header.data_length() > Self::FIXED_PART_SIZE; + + let mut expected_size = Self::FIXED_PART_SIZE; + if read_data_id { + expected_size += 4; + } + + ensure_size!(in: src, size: expected_size); + + let stream_id = src.read_u32(); + let index = src.read_u32(); + let flags = FileContentsFlags::from_bits_truncate(src.read_u32()); + let position_lo = src.read_u32(); + let position_hi = src.read_u32(); + let position = combine_u64(position_lo, position_hi); + let requested_size = src.read_u32(); + let data_id = if read_data_id { Some(src.read_u32()) } else { None }; + + Ok(Self { + stream_id, + index, + flags, + position, + requested_size, + data_id, + }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_data/file_list.rs b/crates/ironrdp-cliprdr/src/pdu/format_data/file_list.rs new file mode 100644 index 00000000..b0940e83 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_data/file_list.rs @@ -0,0 +1,207 @@ +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use ironrdp_pdu::utils::{combine_u64, decode_string, encode_string, split_u64, CharacterSet}; +use ironrdp_pdu::{impl_pdu_pod, write_padding}; + +const NAME_LENGTH: usize = 520; + +bitflags! { + /// Represents `flags` field of `CLIPRDR_FILEDESCRIPTOR` structure. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ClipboardFileFlags: u32 { + /// The fileAttributes field contains valid data. + const ATTRIBUTES = 0x0000_0004; + /// The fileSizeHigh and fileSizeLow fields contain valid data. + const FILE_SIZE = 0x0000_0040; + /// The lastWriteTime field contains valid data. + const LAST_WRITE_TIME = 0x0000_0020; + } +} + +bitflags! { + /// Represents `fileAttributes` of `CLIPRDR_FILEDESCRIPTOR` structure. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ClipboardFileAttributes: u32 { + /// A file that is read-only. Applications can read the file, but cannot write to + /// it or delete it + const READONLY = 0x0000_0001; + /// The file or directory is hidden. It is not included in an ordinary directory + /// listing. + const HIDDEN = 0x0000_0002; + /// A file or directory that the operating system uses a part of, or uses + /// exclusively. + const SYSTEM = 0x0000_0004; + /// Identifies a directory. + const DIRECTORY = 0x0000_0010; + /// A file or directory that is an archive file or directory. Applications typically + /// use this attribute to mark files for backup or removal + const ARCHIVE = 0x0000_0020; + /// A file that does not have other attributes set. This attribute is valid only + /// when used alone. + const NORMAL = 0x0000_0080; + } +} + +/// [2.2.5.2.3.1] File Descriptor (CLIPRDR_FILEDESCRIPTOR) +/// +/// [2.2.5.2.3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeclip/a765d784-2b39-4b88-9faa-88f8666f9c35 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileDescriptor { + pub attributes: Option, + pub last_write_time: Option, + pub file_size: Option, + // TODO: Define a new type for "bounded" strings (this one should never be bigger than 260 characters, including the null-terminator) + pub name: String, +} + +impl_pdu_pod!(FileDescriptor); + +impl FileDescriptor { + const NAME: &'static str = "CLIPRDR_FILEDESCRIPTOR"; + + const FIXED_PART_SIZE: usize = 4 // flags + + 32 // reserved + + 4 // attributes + + 16 // reserved + + 8 // last write time + + 8 // size + + NAME_LENGTH; // name + + const SIZE: usize = Self::FIXED_PART_SIZE; +} + +impl Encode for FileDescriptor { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let mut flags = ClipboardFileFlags::empty(); + if self.attributes.is_some() { + flags |= ClipboardFileFlags::ATTRIBUTES; + } + if self.last_write_time.is_some() { + flags |= ClipboardFileFlags::LAST_WRITE_TIME; + } + if self.file_size.is_some() { + flags |= ClipboardFileFlags::FILE_SIZE; + } + + dst.write_u32(flags.bits()); + dst.write_array([0u8; 32]); + dst.write_u32(self.attributes.unwrap_or(ClipboardFileAttributes::empty()).bits()); + dst.write_array([0u8; 16]); + dst.write_u64(self.last_write_time.unwrap_or_default()); + + let (size_lo, size_hi) = split_u64(self.file_size.unwrap_or_default()); + dst.write_u32(size_hi); + dst.write_u32(size_lo); + + let written = encode_string(dst.remaining_mut(), &self.name, CharacterSet::Unicode, true)?; + dst.advance(written); + + // Pad with zeroes, overriding any previously written data + write_padding!(dst, NAME_LENGTH - written); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for FileDescriptor { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = ClipboardFileFlags::from_bits_truncate(src.read_u32()); + src.read_array::<32>(); + let attributes = if flags.contains(ClipboardFileFlags::ATTRIBUTES) { + Some(ClipboardFileAttributes::from_bits_truncate(src.read_u32())) + } else { + let _ = src.read_u32(); + None + }; + src.read_array::<16>(); + let last_write_time = if flags.contains(ClipboardFileFlags::LAST_WRITE_TIME) { + Some(src.read_u64()) + } else { + let _ = src.read_u64(); + None + }; + let file_size = if flags.contains(ClipboardFileFlags::FILE_SIZE) { + let size_hi = src.read_u32(); + let size_lo = src.read_u32(); + Some(combine_u64(size_lo, size_hi)) + } else { + let _ = src.read_u64(); + None + }; + + let name = decode_string(src.remaining(), CharacterSet::Unicode, true)?; + src.advance(NAME_LENGTH); + + Ok(Self { + attributes, + last_write_time, + file_size, + name, + }) + } +} + +/// Represents `CLIPRDR_FILELIST` +/// +/// NOTE: `Decode` implementation will read all remaining data in cursor as file list. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackedFileList { + pub files: Vec, +} + +impl_pdu_pod!(PackedFileList); + +impl PackedFileList { + const NAME: &'static str = "CLIPRDR_FILELIST"; + const FIXED_PART_SIZE: usize = 4; // file count +} + +impl Encode for PackedFileList { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(cast_length!(Self::NAME, "cItems", self.files.len())?); + + for file in &self.files { + file.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + FileDescriptor::SIZE * self.files.len() + } +} + +impl<'de> Decode<'de> for PackedFileList { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let file_count = cast_length!(Self::NAME, "cItems", src.read_u32())?; + + let mut files = Vec::with_capacity(file_count); + for _ in 0..file_count { + files.push(FileDescriptor::decode(src)?); + } + + Ok(Self { files }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_data/metafile.rs b/crates/ironrdp-cliprdr/src/pdu/format_data/metafile.rs new file mode 100644 index 00000000..433f2e81 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_data/metafile.rs @@ -0,0 +1,107 @@ +use std::borrow::Cow; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, ensure_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; + +bitflags! { + /// Represents `mappingMode` fields of `CLIPRDR_MFPICT` structure. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct PackedMetafileMappingMode: u32 { + /// Each logical unit is mapped to one device pixel. Positive x is to the right; positive + /// y is down. + const TEXT = 0x0000_0001; + /// Each logical unit is mapped to 0.1 millimeter. Positive x is to the right; positive + /// y is up. + const LO_METRIC = 0x0000_0002; + /// Each logical unit is mapped to 0.01 millimeter. Positive x is to the right; positive + /// y is up. + const HI_METRIC = 0x0000_0003; + /// Each logical unit is mapped to 0.01 inch. Positive x is to the right; positive y is up. + const LO_ENGLISH = 0x0000_0004; + /// Each logical unit is mapped to 0.001 inch. Positive x is to the right; positive y is up. + const HI_ENGLISH = 0x0000_0005; + /// Each logical unit is mapped to 1/20 of a printer's point (1/1440 of an inch), also + /// called a twip. Positive x is to the right; positive y is up. + const TWIPS = 0x0000_0006; + /// Logical units are mapped to arbitrary units with equally scaled axes; one unit along + /// the x-axis is equal to one unit along the y-axis. + const ISOTROPIC = 0x0000_0007; + /// Logical units are mapped to arbitrary units with arbitrarily scaled axes. + const ANISOTROPIC = 0x0000_0008; + } +} + +/// Represents `CLIPRDR_MFPICT` +/// +/// NOTE: `Decode` implementation will read all remaining data in cursor as metafile contents. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackedMetafile<'a> { + pub mapping_mode: PackedMetafileMappingMode, + pub x_ext: u32, + pub y_ext: u32, + /// The variable sized contents of the metafile as specified in [MS-WMF] section 2 + pub data: Cow<'a, [u8]>, +} + +impl PackedMetafile<'_> { + const NAME: &'static str = "CLIPRDR_MFPICT"; + const FIXED_PART_SIZE: usize = 4 /* mode */ + 4 /* xExt */ + 4 /* yExt */; + + pub fn new( + mapping_mode: PackedMetafileMappingMode, + x_ext: u32, + y_ext: u32, + data: impl Into>, + ) -> Self { + Self { + mapping_mode, + x_ext, + y_ext, + data: data.into(), + } + } +} + +impl Encode for PackedMetafile<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.mapping_mode.bits()); + dst.write_u32(self.x_ext); + dst.write_u32(self.y_ext); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.data.len() + } +} + +impl<'de> Decode<'de> for PackedMetafile<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let mapping_mode = PackedMetafileMappingMode::from_bits_truncate(src.read_u32()); + let x_ext = src.read_u32(); + let y_ext = src.read_u32(); + + let data_len = src.len(); + + let data = src.read_slice(data_len); + + Ok(Self { + mapping_mode, + x_ext, + y_ext, + data: Cow::Borrowed(data), + }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_data/mod.rs b/crates/ironrdp-cliprdr/src/pdu/format_data/mod.rs new file mode 100644 index 00000000..e2acc1f4 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_data/mod.rs @@ -0,0 +1,254 @@ +mod file_list; +mod metafile; +mod palette; + +pub use self::file_list::*; +pub use self::metafile::*; +pub use self::palette::*; + +#[rustfmt::skip] +use std::borrow::Cow; + +use ironrdp_core::{ + cast_int, ensure_fixed_part_size, ensure_size, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, + WriteCursor, +}; +use ironrdp_pdu::impl_pdu_borrowing; +use ironrdp_pdu::utils::{read_string_from_cursor, to_utf16_bytes, CharacterSet}; + +use super::ClipboardFormatId; +use crate::pdu::{ClipboardPduFlags, PartialHeader}; + +/// Represents `CLIPRDR_FORMAT_DATA_RESPONSE` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FormatDataResponse<'a> { + is_error: bool, + data: Cow<'a, [u8]>, +} + +impl_pdu_borrowing!(FormatDataResponse<'_>, OwnedFormatDataResponse); + +impl IntoOwned for FormatDataResponse<'_> { + type Owned = OwnedFormatDataResponse; + + fn into_owned(self) -> Self::Owned { + OwnedFormatDataResponse { + is_error: self.is_error, + data: Cow::Owned(self.data.into_owned()), + } + } +} + +impl<'a> FormatDataResponse<'a> { + const NAME: &'static str = "CLIPRDR_FORMAT_DATA_RESPONSE"; + + /// Creates new format data response from raw data. + pub fn new_data(data: impl Into>) -> Self { + Self { + is_error: false, + data: data.into(), + } + } + + /// Creates new error format data response. + pub fn new_error() -> Self { + Self { + is_error: true, + data: Cow::Borrowed(&[]), + } + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub fn is_error(&self) -> bool { + self.is_error + } + + /// Creates new format data response from clipboard palette. Please note that this method + /// allocates memory for the data automatically. If you want to avoid this, you can use + /// `new_data` method and encode [`ClipboardPalette`] prior to the call. + pub fn new_palette(palette: &ClipboardPalette) -> EncodeResult { + let mut data = vec![0u8; palette.size()]; + + let mut cursor = WriteCursor::new(&mut data); + palette.encode(&mut cursor)?; + + Ok(Self { + is_error: false, + data: data.into(), + }) + } + + /// Creates new format data response from packed metafile. Please note that this method + /// allocates memory for the data automatically. If you want to avoid this, you can use + /// `new_data` method and encode [`PackedMetafile`] prior to the call. + pub fn new_metafile(metafile: &PackedMetafile<'_>) -> EncodeResult { + let mut data = vec![0u8; metafile.size()]; + + let mut cursor = WriteCursor::new(&mut data); + metafile.encode(&mut cursor)?; + + Ok(Self { + is_error: false, + data: data.into(), + }) + } + + /// Creates new format data response from packed file list. Please note that this method + /// allocates memory for the data automatically. If you want to avoid this, you can use + /// `new_data` method and encode [`PackedFileList`] prior to the call. + pub fn new_file_list(list: &PackedFileList) -> EncodeResult { + let mut data = vec![0u8; list.size()]; + + let mut cursor = WriteCursor::new(&mut data); + list.encode(&mut cursor)?; + + Ok(Self { + is_error: false, + data: data.into(), + }) + } + + /// Creates new format data response from string. + pub fn new_unicode_string(value: &str) -> Self { + let mut encoded = to_utf16_bytes(value); + encoded.push(b'\0'); + encoded.push(b'\0'); + + Self { + is_error: false, + data: encoded.into(), + } + } + + /// Creates new format data response from string. + pub fn new_string(value: &str) -> Self { + let mut encoded = value.as_bytes().to_vec(); + encoded.push(b'\0'); + + Self { + is_error: false, + data: encoded.into(), + } + } + + /// Reads inner data as [`ClipboardPalette`] + pub fn to_palette(&self) -> DecodeResult { + let mut cursor = ReadCursor::new(&self.data); + ClipboardPalette::decode(&mut cursor) + } + + /// Reads inner data as [`PackedMetafile`] + pub fn to_metafile(&self) -> DecodeResult> { + let mut cursor = ReadCursor::new(&self.data); + PackedMetafile::decode(&mut cursor) + } + + /// Reads inner data as [`PackedFileList`] + pub fn to_file_list(&self) -> DecodeResult { + let mut cursor = ReadCursor::new(&self.data); + PackedFileList::decode(&mut cursor) + } + + /// Reads inner data as string + pub fn to_string(&self) -> DecodeResult { + let mut cursor = ReadCursor::new(&self.data); + read_string_from_cursor(&mut cursor, CharacterSet::Ansi, true) + } + + /// Reads inner data as unicode string + pub fn to_unicode_string(&self) -> DecodeResult { + let mut cursor = ReadCursor::new(&self.data); + read_string_from_cursor(&mut cursor, CharacterSet::Unicode, true) + } + + pub fn into_data(self) -> Cow<'a, [u8]> { + self.data + } +} + +impl Encode for FormatDataResponse<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let flags = if self.is_error { + ClipboardPduFlags::RESPONSE_FAIL + } else { + ClipboardPduFlags::RESPONSE_OK + }; + + let header = PartialHeader::new_with_flags(cast_int!("dataLen", self.data.len())?, flags); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.data.len()); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + self.data.len() + } +} + +impl<'de> Decode<'de> for FormatDataResponse<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let header = PartialHeader::decode(src)?; + + let is_error = header.message_flags.contains(ClipboardPduFlags::RESPONSE_FAIL); + + ensure_size!(in: src, size: header.data_length()); + let data = src.read_slice(header.data_length()); + + Ok(Self { + is_error, + data: Cow::Borrowed(data), + }) + } +} + +/// Represents `CLIPRDR_FORMAT_DATA_REQUEST` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FormatDataRequest { + pub format: ClipboardFormatId, +} + +impl FormatDataRequest { + const NAME: &'static str = "CLIPRDR_FORMAT_DATA_REQUEST"; + const FIXED_PART_SIZE: usize = 4 /* format */; +} + +impl Encode for FormatDataRequest { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", Self::FIXED_PART_SIZE)?); + header.encode(dst)?; + + ensure_fixed_part_size!(in: dst); + dst.write_u32(self.format.value()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for FormatDataRequest { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let _header = PartialHeader::decode(src)?; + + ensure_fixed_part_size!(in: src); + let format = ClipboardFormatId::new(src.read_u32()); + + Ok(Self { format }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_data/palette.rs b/crates/ironrdp-cliprdr/src/pdu/format_data/palette.rs new file mode 100644 index 00000000..37c8cb3c --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_data/palette.rs @@ -0,0 +1,73 @@ +use ironrdp_core::{Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; +use ironrdp_pdu::impl_pdu_pod; + +/// Represents `PALETTEENTRY` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PaletteEntry { + pub red: u8, + pub green: u8, + pub blue: u8, + pub extra: u8, +} + +impl PaletteEntry { + const SIZE: usize = 1 /* R */ + 1 /* G */ + 1 /* B */ + 1 /* extra */; +} + +/// Represents `CLIPRDR_PALETTE` +/// +/// NOTE: `Decode` implementation will read all remaining data in cursor as the palette entries. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClipboardPalette { + pub entries: Vec, +} + +impl_pdu_pod!(ClipboardPalette); + +impl ClipboardPalette { + const NAME: &'static str = "CLIPRDR_PALETTE"; +} + +impl Encode for ClipboardPalette { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + for entry in &self.entries { + dst.write_u8(entry.red); + dst.write_u8(entry.green); + dst.write_u8(entry.blue); + dst.write_u8(entry.extra); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.entries.len() * PaletteEntry::SIZE + } +} + +impl<'de> Decode<'de> for ClipboardPalette { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let entries_count = src.len() / PaletteEntry::SIZE; + + let mut entries = Vec::with_capacity(entries_count); + for _ in 0..entries_count { + let red = src.read_u8(); + let green = src.read_u8(); + let blue = src.read_u8(); + let extra = src.read_u8(); + + entries.push(PaletteEntry { + red, + green, + blue, + extra, + }); + } + + Ok(Self { entries }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_list.rs b/crates/ironrdp-cliprdr/src/pdu/format_list.rs new file mode 100644 index 00000000..1060b7f4 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_list.rs @@ -0,0 +1,455 @@ +use std::borrow::Cow; + +use ironrdp_core::{ + cast_int, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, + WriteCursor, +}; +use ironrdp_pdu::utils::{read_string_from_cursor, to_utf16_bytes, write_string_to_cursor, CharacterSet}; +use ironrdp_pdu::{decode_err, impl_pdu_borrowing, impl_pdu_pod, PduResult}; + +use crate::pdu::{ClipboardPduFlags, PartialHeader}; + +/// Clipboard format id. +/// +/// [Standard clipboard formats](https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats) +/// defined by Microsoft are available as constants. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ClipboardFormatId(pub u32); + +impl ClipboardFormatId { + /// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. + /// A null character signals the end of the data. Use this format for ANSI text. + pub const CF_TEXT: Self = Self(1); + + /// A handle to a bitmap (HBITMAP). + pub const CF_BITMAP: Self = Self(2); + + /// Handle to a metafile picture format as defined by the METAFILEPICT structure. + /// + /// When passing a CF_METAFILEPICT handle by means of DDE, the application responsible for + /// deleting hMem should also free the metafile referred to by the CF_METAFILEPICT handle. + pub const CF_METAFILEPICT: Self = Self(3); + + /// Microsoft Symbolic Link (SYLK) format. + pub const CF_SYLK: Self = Self(4); + + /// Software Arts' Data Interchange Format. + pub const CF_DIF: Self = Self(5); + + /// Tagged-image file format. + pub const CF_TIFF: Self = Self(6); + + /// Text format containing characters in the OEM character set. Each line ends with a carriage + /// return/linefeed (CR-LF) combination. A null character signals the end of the data. + pub const CF_OEMTEXT: Self = Self(7); + + /// A memory object containing a BITMAPINFO structure followed by the bitmap bits. + pub const CF_DIB: Self = Self(8); + + /// Handle to a color palette. + /// + /// Whenever an application places data in the clipboard that + /// depends on or assumes a color palette, it should place the palette on the clipboard as well. + /// If the clipboard contains data in the CF_PALETTE (logical color palette) format, the + /// application should use the SelectPalette and RealizePalette functions to realize (compare) + /// any other data in the clipboard against that logical palette. When displaying clipboard + /// data, the clipboard always uses as its current palette any object on the clipboard that is + /// in the CF_PALETTE format. + /// + /// NOTE: When transferred over `CLIPRDR`, [`crate::pdu::format_data::ClipboardPalette`] structure + /// is used instead of `HPALETTE`. + pub const CF_PALETTE: Self = Self(9); + + /// Data for the pen extensions to the Microsoft Windows for Pen Computing. + pub const CF_PENDATA: Self = Self(10); + + /// Represents audio data more complex than can be represented in a CF_WAVE standard wave format. + pub const CF_RIFF: Self = Self(11); + + /// Represents audio data in one of the standard wave formats, such as 11 kHz or 22 kHz PCM. + pub const CF_WAVE: Self = Self(12); + + /// Unicode text format. Each line ends with a carriage return/linefeed (CR-LF) combination. + /// A null character signals the end of the data. + pub const CF_UNICODETEXT: Self = Self(13); + + /// A handle to an enhanced metafile (HENHMETAFILE). + /// + /// NOTE: When transferred over `CLIPRDR`, [`crate::pdu::format_data::PackedMetafile`] structure + /// is used instead of `HENHMETAFILE`. + pub const CF_ENHMETAFILE: Self = Self(14); + + /// A handle to type HDROP that identifies a list of files. An application can retrieve + /// information about the files by passing the handle to the DragQueryFile function. + pub const CF_HDROP: Self = Self(15); + + /// The data is a handle (HGLOBAL) to the locale identifier (LCID) associated with text in the + /// clipboard. + /// + /// When you close the clipboard, if it contains CF_TEXT data but no CF_LOCALE data, + /// the system automatically sets the CF_LOCALE format to the current input language. You can + /// use the CF_LOCALE format to associate a different locale with the clipboard text. An + /// application that pastes text from the clipboard can retrieve this format to determine which + /// character set was used to generate the text. Note that the clipboard does not support plain + /// text in multiple character sets. To achieve this, use a formatted text data type such as + /// RTF instead.The system uses the code page associated with CF_LOCALE to implicitly convert + /// from CF_TEXT to CF_UNICODETEXT. Therefore, the correct code page table is used for the + /// conversion. + pub const CF_LOCALE: Self = Self(16); + + /// A memory object containing a BITMAPV5HEADER structure followed by the bitmap color space + /// information and the bitmap bits. + pub const CF_DIBV5: Self = Self(17); + + /// Creates new `ClipboardFormatId` with given id. Note that [`ClipboardFormatId`] already + /// defines constants for standard clipboard formats, [`Self::new`] should only be + /// used for custom/OS-specific formats. + pub fn new(id: u32) -> Self { + Self(id) + } + + pub fn value(&self) -> u32 { + self.0 + } + + pub fn is_standard(self) -> bool { + matches!( + self, + Self::CF_TEXT + | Self::CF_BITMAP + | Self::CF_METAFILEPICT + | Self::CF_SYLK + | Self::CF_DIF + | Self::CF_TIFF + | Self::CF_OEMTEXT + | Self::CF_DIB + | Self::CF_PALETTE + | Self::CF_PENDATA + | Self::CF_RIFF + | Self::CF_WAVE + | Self::CF_UNICODETEXT + | Self::CF_ENHMETAFILE + | Self::CF_HDROP + | Self::CF_LOCALE + | Self::CF_DIBV5 + ) + } + + pub fn is_registered(self) -> bool { + (self.0 >= 0xC000) && (self.0 <= 0xFFFF) + } +} + +/// Clipboard format name. Hardcoded format names defined by [MS-RDPECLIP] are available as +/// constants. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClipboardFormatName(Cow<'static, str>); + +impl ClipboardFormatName { + /// Special format name for file lists defined by [`MS-RDPECLIP`] which is used for clipboard + /// data with [`crate::pdu::format_data::PackedFileList`] payload. + pub const FILE_LIST: Self = Self::new_static("FileGroupDescriptorW"); + + /// Special format defined by Windows to store HTML fragment in clipboard. + pub const HTML: Self = Self::new_static("HTML Format"); + + pub fn new(name: impl Into>) -> Self { + Self(name.into()) + } + + /// Same as [`Self::new`], but for `'static` string - it can be used in const contexts. + pub const fn new_static(name: &'static str) -> Self { + Self(Cow::Borrowed(name)) + } + + pub fn value(&self) -> &str { + &self.0 + } +} + +/// Represents `CLIPRDR_SHORT_FORMAT_NAME` and `CLIPRDR_LONG_FORMAT_NAME` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClipboardFormat { + pub id: ClipboardFormatId, + pub name: Option, +} + +impl ClipboardFormat { + /// Creates unnamed `ClipboardFormat` with given id. + pub const fn new(id: ClipboardFormatId) -> Self { + Self { id, name: None } + } + + /// Sets clipboard format name. + /// + /// This is typically used for custom/OS-specific formats where a name must be associated to + /// the `ClipboardFormatId` in order to distinguish between vendors. + #[must_use] + pub fn with_name(self, name: ClipboardFormatName) -> Self { + if name.0.is_empty() { + return Self { + id: self.id, + name: None, + }; + } + + Self { + id: self.id, + name: Some(name), + } + } + + #[inline] + pub fn id(&self) -> ClipboardFormatId { + self.id + } + + #[inline] + pub fn name(&self) -> Option<&ClipboardFormatName> { + self.name.as_ref() + } +} + +/// Represents `CLIPRDR_FORMAT_LIST` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FormatList<'a> { + use_ascii: bool, + encoded_formats: Cow<'a, [u8]>, +} + +impl_pdu_borrowing!(FormatList<'_>, OwnedFormatList); + +impl IntoOwned for FormatList<'_> { + type Owned = OwnedFormatList; + + fn into_owned(self) -> Self::Owned { + OwnedFormatList { + use_ascii: self.use_ascii, + encoded_formats: Cow::Owned(self.encoded_formats.into_owned()), + } + } +} + +impl FormatList<'_> { + const NAME: &'static str = "CLIPRDR_FORMAT_LIST"; + + // `CLIPRDR_SHORT_FORMAT_NAME` size + const SHORT_FORMAT_SIZE: usize = 4 /* formatId */ + 32 /* name */; + + fn new_impl(formats: &[ClipboardFormat], use_long_format: bool, use_ascii: bool) -> EncodeResult { + let charset = if use_ascii { + CharacterSet::Ansi + } else { + CharacterSet::Unicode + }; + + let mut bytes_written = 0; + + if use_long_format { + // Sane default for formats buffer size to avoid reallocations + const DEFAULT_STRING_BUFFER_SIZE: usize = 1024; + let mut buffer = vec![0u8; DEFAULT_STRING_BUFFER_SIZE]; + + for format in formats { + let encoded_string = match charset { + CharacterSet::Ansi => { + let mut str_buffer = format + .name + .as_ref() + .map(|name| name.value().as_bytes().to_vec()) + .unwrap_or_default(); + str_buffer.push(b'\0'); + str_buffer + } + CharacterSet::Unicode => { + let mut str_buffer = format + .name + .as_ref() + .map(|name| to_utf16_bytes(name.value())) + .unwrap_or_default(); + str_buffer.push(b'\0'); + str_buffer.push(b'\0'); + str_buffer + } + }; + + let required_size = 4 + encoded_string.len(); + if buffer.len() - bytes_written < required_size { + buffer.resize(bytes_written + required_size, 0); + } + + let mut cursor = WriteCursor::new(&mut buffer[bytes_written..]); + + // Write will never fail, as we pre-allocated space in buffer + cursor.write_u32(format.id.value()); + cursor.write_slice(&encoded_string); + + bytes_written += required_size; + } + + buffer.truncate(bytes_written); + + Ok(Self { + use_ascii, + encoded_formats: Cow::Owned(buffer), + }) + } else { + let mut buffer = vec![0u8; Self::SHORT_FORMAT_SIZE * formats.len()]; + for (idx, format) in formats.iter().enumerate() { + let mut cursor = WriteCursor::new(&mut buffer[idx * Self::SHORT_FORMAT_SIZE..]); + cursor.write_u32(format.id.value()); + write_string_to_cursor( + &mut cursor, + format.name.as_ref().map(|name| name.value()).unwrap_or_default(), + charset, + true, + )?; + } + + Ok(Self { + use_ascii, + encoded_formats: Cow::Owned(buffer), + }) + } + } + + pub fn new_unicode(formats: &[ClipboardFormat], use_long_format: bool) -> EncodeResult { + Self::new_impl(formats, use_long_format, false) + } + + pub fn new_ascii(formats: &[ClipboardFormat], use_long_format: bool) -> EncodeResult { + Self::new_impl(formats, use_long_format, true) + } + + pub fn get_formats(&self, use_long_format: bool) -> PduResult> { + let mut src = ReadCursor::new(self.encoded_formats.as_ref()); + let charset = if self.use_ascii { + CharacterSet::Ansi + } else { + CharacterSet::Unicode + }; + + if use_long_format { + // Minimal `CLIPRDR_LONG_FORMAT_NAME` size (id + null-terminated name) + const MINIMAL_FORMAT_SIZE: usize = 4 /* id */ + 2 /* null-terminated name */; + + let mut formats = Vec::with_capacity(16); + + while src.len() >= MINIMAL_FORMAT_SIZE { + let id = src.read_u32(); + let name = read_string_from_cursor(&mut src, charset, true).map_err(|e| decode_err!(e))?; + + let format = ClipboardFormat::new(ClipboardFormatId::new(id)).with_name(ClipboardFormatName::new(name)); + + formats.push(format); + } + + Ok(formats) + } else { + let items_count = src.len() / Self::SHORT_FORMAT_SIZE; + + let mut formats = Vec::with_capacity(items_count); + + for _ in 0..items_count { + let id = src.read_u32(); + let name_buffer = src.read_slice(32); + + let mut name_cursor: ReadCursor<'_> = ReadCursor::new(name_buffer); + let name = read_string_from_cursor(&mut name_cursor, charset, true).map_err(|e| decode_err!(e))?; + + let format = ClipboardFormat::new(ClipboardFormatId(id)).with_name(ClipboardFormatName::new(name)); + + formats.push(format); + } + + Ok(formats) + } + } +} + +impl<'de> Decode<'de> for FormatList<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let header = PartialHeader::decode(src)?; + + let use_ascii = header.message_flags.contains(ClipboardPduFlags::ASCII_NAMES); + ensure_size!(in: src, size: header.data_length()); + + let encoded_formats = src.read_slice(header.data_length()); + + Ok(Self { + use_ascii, + encoded_formats: Cow::Borrowed(encoded_formats), + }) + } +} + +impl Encode for FormatList<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header_flags = if self.use_ascii { + ClipboardPduFlags::ASCII_NAMES + } else { + ClipboardPduFlags::empty() + }; + + let header = PartialHeader::new_with_flags(cast_int!("dataLen", self.encoded_formats.len())?, header_flags); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.encoded_formats.len()); + + dst.write_slice(&self.encoded_formats); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + self.encoded_formats.len() + } +} + +/// Represents `FORMAT_LIST_RESPONSE` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatListResponse { + Ok, + Fail, +} + +impl_pdu_pod!(FormatListResponse); + +impl FormatListResponse { + const NAME: &'static str = "FORMAT_LIST_RESPONSE"; +} + +impl Encode for FormatListResponse { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header_flags = match self { + FormatListResponse::Ok => ClipboardPduFlags::RESPONSE_OK, + FormatListResponse::Fail => ClipboardPduFlags::RESPONSE_FAIL, + }; + + let header = PartialHeader::new_with_flags(0, header_flags); + header.encode(dst) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + } +} + +impl<'de> Decode<'de> for FormatListResponse { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let header = PartialHeader::decode(src)?; + match header.message_flags { + ClipboardPduFlags::RESPONSE_OK => Ok(FormatListResponse::Ok), + ClipboardPduFlags::RESPONSE_FAIL => Ok(FormatListResponse::Fail), + _ => Err(invalid_field_err!("msgFlags", "Invalid format list message flags")), + } + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/lock.rs b/crates/ironrdp-cliprdr/src/pdu/lock.rs new file mode 100644 index 00000000..58f6b379 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/lock.rs @@ -0,0 +1,48 @@ +use ironrdp_core::{ + cast_int, ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use ironrdp_pdu::impl_pdu_pod; + +use crate::pdu::PartialHeader; + +/// Represents `CLIPRDR_LOCK_CLIPDATA`/`CLIPRDR_UNLOCK_CLIPDATA` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LockDataId(pub u32); + +impl_pdu_pod!(LockDataId); + +impl LockDataId { + const NAME: &'static str = "CLIPRDR_(UN)LOCK_CLIPDATA"; + const FIXED_PART_SIZE: usize = 4 /* Id */; +} + +impl Encode for LockDataId { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", Self::FIXED_PART_SIZE)?); + header.encode(dst)?; + + ensure_fixed_part_size!(in: dst); + dst.write_u32(self.0); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for LockDataId { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let _header = PartialHeader::decode(src)?; + + ensure_fixed_part_size!(in: src); + let id = src.read_u32(); + + Ok(Self(id)) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/mod.rs b/crates/ironrdp-cliprdr/src/pdu/mod.rs new file mode 100644 index 00000000..3fbcdc1f --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/mod.rs @@ -0,0 +1,270 @@ +//! This module implements RDP clipboard channel PDUs encode/decode logic as defined in +//! [MS-RDPECLIP]: Remote Desktop Protocol: Clipboard Virtual Channel Extension + +mod capabilities; +mod client_temporary_directory; +mod file_contents; +mod format_data; +mod format_list; +mod lock; + +pub use self::capabilities::*; +pub use self::client_temporary_directory::*; +pub use self::file_contents::*; +pub use self::format_data::*; +pub use self::format_list::*; +pub use self::lock::*; + +#[rustfmt::skip] +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use ironrdp_svc::SvcEncode; + +const MSG_TYPE_MONITOR_READY: u16 = 0x0001; +const MSG_TYPE_FORMAT_LIST: u16 = 0x0002; +const MSG_TYPE_FORMAT_LIST_RESPONSE: u16 = 0x0003; +const MSG_TYPE_FORMAT_DATA_REQUEST: u16 = 0x0004; +const MSG_TYPE_FORMAT_DATA_RESPONSE: u16 = 0x0005; +const MSG_TYPE_TEMPORARY_DIRECTORY: u16 = 0x0006; +const MSG_TYPE_CAPABILITIES: u16 = 0x0007; +const MSG_TYPE_FILE_CONTENTS_REQUEST: u16 = 0x0008; +const MSG_TYPE_FILE_CONTENTS_RESPONSE: u16 = 0x0009; +const MSG_TYPE_LOCK_CLIPDATA: u16 = 0x000A; +const MSG_TYPE_UNLOCK_CLIPDATA: u16 = 0x000B; + +pub const FORMAT_ID_PALETTE: u32 = 9; +pub const FORMAT_ID_METAFILE: u32 = 3; +pub const FORMAT_NAME_FILE_LIST: &str = "FileGroupDescriptorW"; + +/// Header without message type included +struct PartialHeader { + message_flags: ClipboardPduFlags, + data_length: u32, +} + +impl PartialHeader { + const NAME: &'static str = "CLIPRDR_HEADER"; + const FIXED_PART_SIZE: usize = 2 /* flags */ + 4 /* len */; + const SIZE: usize = Self::FIXED_PART_SIZE; + + pub(crate) fn new(inner_data_length: u32) -> Self { + Self::new_with_flags(inner_data_length, ClipboardPduFlags::empty()) + } + + pub(crate) fn new_with_flags(data_length: u32, message_flags: ClipboardPduFlags) -> Self { + Self { + message_flags, + data_length, + } + } + + pub(crate) fn data_length(&self) -> usize { + usize::try_from(self.data_length).expect("BUG: Upcasting u32 -> usize should be infallible") + } +} + +impl<'de> Decode<'de> for PartialHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let message_flags = ClipboardPduFlags::from_bits_truncate(src.read_u16()); + let data_length = src.read_u32(); + + Ok(Self { + message_flags, + data_length, + }) + } +} + +impl Encode for PartialHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.message_flags.bits()); + dst.write_u32(self.data_length); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +/// Clipboard channel message PDU +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ClipboardPdu<'a> { + MonitorReady, + FormatList(FormatList<'a>), + FormatListResponse(FormatListResponse), + FormatDataRequest(FormatDataRequest), + FormatDataResponse(FormatDataResponse<'a>), + TemporaryDirectory(ClientTemporaryDirectory<'a>), + Capabilities(Capabilities), + FileContentsRequest(FileContentsRequest), + FileContentsResponse(FileContentsResponse<'a>), + LockData(LockDataId), + UnlockData(LockDataId), +} + +impl ClipboardPdu<'_> { + const NAME: &'static str = "ClipboardPdu"; + const FIXED_PART_SIZE: usize = 2 /* type */; + + pub fn message_name(&self) -> &'static str { + match self { + ClipboardPdu::MonitorReady => "CLIPRDR_MONITOR_READY", + ClipboardPdu::FormatList(_) => "CLIPRDR_FORMAT_LIST", + ClipboardPdu::FormatListResponse(_) => "CLIPRDR_FORMAT_LIST_RESPONSE", + ClipboardPdu::FormatDataRequest(_) => "CLIPRDR_FORMAT_DATA_REQUEST", + ClipboardPdu::FormatDataResponse(_) => "CLIPRDR_FORMAT_DATA_RESPONSE", + ClipboardPdu::TemporaryDirectory(_) => "CLIPRDR_TEMP_DIRECTORY", + ClipboardPdu::Capabilities(_) => "CLIPRDR_CAPABILITIES", + ClipboardPdu::FileContentsRequest(_) => "CLIPRDR_FILECONTENTS_REQUEST", + ClipboardPdu::FileContentsResponse(_) => "CLIPRDR_FILECONTENTS_RESPONSE", + ClipboardPdu::LockData(_) => "CLIPRDR_LOCK_CLIPDATA", + ClipboardPdu::UnlockData(_) => "CLIPRDR_UNLOCK_CLIPDATA", + } + } +} + +impl Encode for ClipboardPdu<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let write_empty_pdu = |dst: &mut WriteCursor<'_>| { + let header = PartialHeader::new(0); + header.encode(dst) + }; + + match self { + ClipboardPdu::MonitorReady => { + dst.write_u16(MSG_TYPE_MONITOR_READY); + write_empty_pdu(dst) + } + ClipboardPdu::FormatList(pdu) => { + dst.write_u16(MSG_TYPE_FORMAT_LIST); + pdu.encode(dst) + } + ClipboardPdu::FormatListResponse(pdu) => { + dst.write_u16(MSG_TYPE_FORMAT_LIST_RESPONSE); + pdu.encode(dst) + } + ClipboardPdu::FormatDataRequest(pdu) => { + dst.write_u16(MSG_TYPE_FORMAT_DATA_REQUEST); + pdu.encode(dst) + } + ClipboardPdu::FormatDataResponse(pdu) => { + dst.write_u16(MSG_TYPE_FORMAT_DATA_RESPONSE); + pdu.encode(dst) + } + ClipboardPdu::TemporaryDirectory(pdu) => { + dst.write_u16(MSG_TYPE_TEMPORARY_DIRECTORY); + pdu.encode(dst) + } + ClipboardPdu::Capabilities(pdu) => { + dst.write_u16(MSG_TYPE_CAPABILITIES); + pdu.encode(dst) + } + ClipboardPdu::FileContentsRequest(pdu) => { + dst.write_u16(MSG_TYPE_FILE_CONTENTS_REQUEST); + pdu.encode(dst) + } + ClipboardPdu::FileContentsResponse(pdu) => { + dst.write_u16(MSG_TYPE_FILE_CONTENTS_RESPONSE); + pdu.encode(dst) + } + ClipboardPdu::LockData(pdu) => { + dst.write_u16(MSG_TYPE_LOCK_CLIPDATA); + pdu.encode(dst) + } + ClipboardPdu::UnlockData(pdu) => { + dst.write_u16(MSG_TYPE_UNLOCK_CLIPDATA); + pdu.encode(dst) + } + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let empty_size = PartialHeader::SIZE; + + let variable_size = match self { + ClipboardPdu::MonitorReady => empty_size, + ClipboardPdu::FormatList(pdu) => pdu.size(), + ClipboardPdu::FormatListResponse(pdu) => pdu.size(), + ClipboardPdu::FormatDataRequest(pdu) => pdu.size(), + ClipboardPdu::FormatDataResponse(pdu) => pdu.size(), + ClipboardPdu::TemporaryDirectory(pdu) => pdu.size(), + ClipboardPdu::Capabilities(pdu) => pdu.size(), + ClipboardPdu::FileContentsRequest(pdu) => pdu.size(), + ClipboardPdu::FileContentsResponse(pdu) => pdu.size(), + ClipboardPdu::LockData(pdu) => pdu.size(), + ClipboardPdu::UnlockData(pdu) => pdu.size(), + }; + + Self::FIXED_PART_SIZE + variable_size + } +} + +impl SvcEncode for ClipboardPdu<'_> {} + +impl<'de> Decode<'de> for ClipboardPdu<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let read_empty_pdu = |src: &mut ReadCursor<'de>| -> DecodeResult<()> { + let _header = PartialHeader::decode(src)?; + Ok(()) + }; + + let pdu = match src.read_u16() { + MSG_TYPE_MONITOR_READY => { + read_empty_pdu(src)?; + ClipboardPdu::MonitorReady + } + MSG_TYPE_FORMAT_LIST => ClipboardPdu::FormatList(FormatList::decode(src)?), + MSG_TYPE_FORMAT_LIST_RESPONSE => ClipboardPdu::FormatListResponse(FormatListResponse::decode(src)?), + MSG_TYPE_FORMAT_DATA_REQUEST => ClipboardPdu::FormatDataRequest(FormatDataRequest::decode(src)?), + MSG_TYPE_FORMAT_DATA_RESPONSE => ClipboardPdu::FormatDataResponse(FormatDataResponse::decode(src)?), + MSG_TYPE_TEMPORARY_DIRECTORY => ClipboardPdu::TemporaryDirectory(ClientTemporaryDirectory::decode(src)?), + MSG_TYPE_CAPABILITIES => ClipboardPdu::Capabilities(Capabilities::decode(src)?), + MSG_TYPE_FILE_CONTENTS_REQUEST => ClipboardPdu::FileContentsRequest(FileContentsRequest::decode(src)?), + MSG_TYPE_FILE_CONTENTS_RESPONSE => ClipboardPdu::FileContentsResponse(FileContentsResponse::decode(src)?), + MSG_TYPE_LOCK_CLIPDATA => ClipboardPdu::LockData(LockDataId::decode(src)?), + MSG_TYPE_UNLOCK_CLIPDATA => ClipboardPdu::UnlockData(LockDataId::decode(src)?), + _ => return Err(invalid_field_err!("msgType", "Unknown clipboard PDU type")), + }; + + Ok(pdu) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + /// Represents `msgFlags` field of `CLIPRDR_HEADER` structure + pub struct ClipboardPduFlags: u16 { + /// Used by the Format List Response PDU, Format Data Response PDU, and File + /// Contents Response PDU to indicate that the associated request Format List PDU, + /// Format Data Request PDU, and File Contents Request PDU were processed + /// successfully + const RESPONSE_OK = 0x0001; + /// Used by the Format List Response PDU, Format Data Response PDU, and File + /// Contents Response PDU to indicate that the associated Format List PDU, Format + /// Data Request PDU, and File Contents Request PDU were not processed successful + const RESPONSE_FAIL = 0x0002; + /// Used by the Short Format Name variant of the Format List Response PDU to indicate + /// that the format names are in ASCII 8 + const ASCII_NAMES = 0x0004; + } +} diff --git a/crates/ironrdp-connector/CHANGELOG.md b/crates/ironrdp-connector/CHANGELOG.md new file mode 100644 index 00000000..888acabe --- /dev/null +++ b/crates/ironrdp-connector/CHANGELOG.md @@ -0,0 +1,135 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.7.1...ironrdp-connector-v0.8.0)] - 2025-12-18 + +### Build + +- Bump picky and sspi ([#1028](https://github.com/Devolutions/IronRDP/issues/1028)) ([5bd319126d](https://github.com/Devolutions/IronRDP/commit/5bd319126d32fbd8e505508e27ab2b1a18a83d04)) + + This fixes build issues with some dependencies. + +## [[0.7.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.7.0...ironrdp-connector-v0.7.1)] - 2025-09-04 + +### Features + +- Add API to retrieve registered SVC processors (#938) ([17833fe009](https://github.com/Devolutions/IronRDP/commit/17833fe009279823c4076d3e2e0c7d063fd24a43)) + +## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.6.0...ironrdp-connector-v0.7.0)] - 2025-08-29 + +### Features + +- Add QOI image codec ([613fd51f26](https://github.com/Devolutions/IronRDP/commit/613fd51f26315d8212662c46f8e625c541e4bb59)) + + The Quite OK Image format ([1]) losslessly compresses images to a similar size + of PNG, while offering 20x-50x faster encoding and 3x-4x faster decoding. + +- Add QOIZ image codec ([87df67fdc7](https://github.com/Devolutions/IronRDP/commit/87df67fdc76ff4f39d4b83521e34bf3b5e2e73bb)) + + Add a new QOIZ codec for SetSurface command. The PDU data contains the same + data as the QOI codec, with zstd compression. + +- Add an option to specify a timezone (#917) ([6fab9f8228](https://github.com/Devolutions/IronRDP/commit/6fab9f8228578b3c78db131b3c2e0526352116a9)) + +### Bug Fixes + +- [**breaking**] Rename option no_server_pointer into enable_server_pointer ([218fed03c7](https://github.com/Devolutions/IronRDP/commit/218fed03c7993af0f958453e3944c58bcf9f43cb)) + +- [**breaking**] Rename option no_audio_playback into enable_audio_playback ([5d8a487001](https://github.com/Devolutions/IronRDP/commit/5d8a487001c1280cbaf9f581f2a9a2f47d187bf0)) + +### Build + +- Bump rand to 0.9 ([de0877188c](https://github.com/Devolutions/IronRDP/commit/de0877188cbb3692c3ce0d9a72f6e96d515cde1f)) + +- Bump picky from 7.0.0-rc.16 to 7.0.0-rc.17 (#941) ([fe31cf2c57](https://github.com/Devolutions/IronRDP/commit/fe31cf2c574e0b06177a931db4cac95ea9cfbe7e)) + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.5.1...ironrdp-connector-v0.6.0)] - 2025-07-08 + +### Build + +- [**breaking**] Update sspi dependency (#839) ([33530212c4](https://github.com/Devolutions/IronRDP/commit/33530212c42bf28c875ac078ed2408657831b417)) + +## [[0.5.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.5.0...ironrdp-connector-v0.5.1)] - 2025-07-03 + +### Build + +- Bump picky to v7.0.0-rc.15 (#850) ([eca256ae10](https://github.com/Devolutions/IronRDP/commit/eca256ae10c52c4a42e7e77d41c0a1d6c180ebf3)) + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.4.0...ironrdp-connector-v0.5.0)] - 2025-05-27 + +### Features + +- Add no_audio_playback flag to Config struct ([9f0edcc4c9](https://github.com/Devolutions/IronRDP/commit/9f0edcc4c9c49d59cc10de37f920aae073e3dd8a)) + + Enable audio playback on the client. + +### Bug Fixes + +- [**breaking**] Fix name of client address field (#754) ([bdde2c76de](https://github.com/Devolutions/IronRDP/commit/bdde2c76ded7315f7bc91d81a0909a1cb827d870)) + +- Inject socket local address for the client addr (#759) ([712da42ded](https://github.com/Devolutions/IronRDP/commit/712da42dedc193239e457d8270d33cc70bd6a4b9)) + + We used to inject the resolved target server address, but that is not + what is expected. Server typically ignores this field so this was not a + problem up until now. + +### Refactor + +- [**breaking**] Add supported codecs in BitmapConfig ([f03ee393a3](https://github.com/Devolutions/IronRDP/commit/f03ee393a36906114b5bcba0e88ebc6869a99785)) + + + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.3.2...ironrdp-connector-v0.4.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + + +## [[0.3.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.3.1...ironrdp-connector-v0.3.2)] - 2025-03-07 + +### Build + +- Update dependencies + + + +## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.3.0...ironrdp-connector-v0.3.1)] - 2025-01-30 + +### Bug Fixes + +- Decrease log verbosity for license exchange ([#655](https://github.com/Devolutions/IronRDP/issues/655)) ([c8597733fe](https://github.com/Devolutions/IronRDP/commit/c8597733fe9998318764064c3682506bf82026d2)) + + + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.2.2...ironrdp-connector-v0.3.0)] - 2025-01-28 + +### Features + +- Support license caching ([#634](https://github.com/Devolutions/IronRDP/issues/634)) ([dd221bf224](https://github.com/Devolutions/IronRDP/commit/dd221bf22401c4635798ec012724cba7e6d503b2)) + + Adds support for license caching by storing the license obtained + from SERVER_UPGRADE_LICENSE message and sending + CLIENT_LICENSE_INFO if a license requested by the server is already + stored in the cache. + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo ([#631](https://github.com/Devolutions/IronRDP/issues/631)) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +### Build + +- Bump picky from 7.0.0-rc.11 to 7.0.0-rc.12 ([#639](https://github.com/Devolutions/IronRDP/issues/639)) ([a16a131e43](https://github.com/Devolutions/IronRDP/commit/a16a131e4301e0dfafe8f3b73e1a75a3a06cfdc7)) + + + +## [[0.2.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.2.1...ironrdp-connector-v0.2.2)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-connector/Cargo.toml b/crates/ironrdp-connector/Cargo.toml new file mode 100644 index 00000000..8119bab4 --- /dev/null +++ b/crates/ironrdp-connector/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "ironrdp-connector" +version = "0.8.0" +readme = "README.md" +description = "State machines to drive an RDP connection sequence" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +arbitrary = ["dep:arbitrary"] +qoi = ["ironrdp-pdu/qoi"] +qoiz = ["ironrdp-pdu/qoiz"] + +[dependencies] +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public +ironrdp-error = { path = "../ironrdp-error", version = "0.1" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6", features = ["std"] } # public +arbitrary = { version = "1", features = ["derive"], optional = true } # public +sspi = { version = "0.18", features = ["scard"] } +url = "2.5" # public +rand = { version = "0.9", features = ["std"] } # TODO: dependency injection? +tracing = { version = "0.1", features = ["log"] } +picky-asn1-der = "0.5" +picky-asn1-x509 = "0.15" +picky = "=7.0.0-rc.20" # FIXME: We are pinning with = because the candidate version number counts as the minor number by Cargo, and will be automatically bumped in the Cargo.lock. + +[lints] +workspace = true diff --git a/crates/ironrdp-connector/LICENSE-APACHE b/crates/ironrdp-connector/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-connector/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-connector/LICENSE-MIT b/crates/ironrdp-connector/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-connector/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-connector/README.md b/crates/ironrdp-connector/README.md new file mode 100644 index 00000000..4c42afad --- /dev/null +++ b/crates/ironrdp-connector/README.md @@ -0,0 +1,7 @@ +# IronRDP Connector + +Abstract state machine to drive an RDP connection sequence. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-connector/src/channel_connection.rs b/crates/ironrdp-connector/src/channel_connection.rs new file mode 100644 index 00000000..eb2908a4 --- /dev/null +++ b/crates/ironrdp-connector/src/channel_connection.rs @@ -0,0 +1,262 @@ +use core::mem; +use std::collections::HashSet; + +use ironrdp_core::WriteBuf; +use ironrdp_pdu::x224::X224; +use ironrdp_pdu::{mcs, PduHint}; +use tracing::{debug, warn}; + +use crate::{ + general_err, reason_err, ConnectorError, ConnectorErrorExt as _, ConnectorResult, Sequence, State, Written, +}; + +#[derive(Default, Debug)] +#[non_exhaustive] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum ChannelConnectionState { + #[default] + Consumed, + + SendErectDomainRequest, + SendAttachUserRequest, + WaitAttachUserConfirm, + SendChannelJoinRequest { + user_channel_id: u16, + join_channel_ids: HashSet, + }, + WaitChannelJoinConfirm { + user_channel_id: u16, + remaining_channel_ids: HashSet, + }, + AllJoined { + user_channel_id: u16, + }, +} + +impl State for ChannelConnectionState { + fn name(&self) -> &'static str { + match self { + Self::Consumed => "Consumed", + Self::SendErectDomainRequest => "SendErectDomainRequest", + Self::SendAttachUserRequest => "SendAttachUserRequest", + Self::WaitAttachUserConfirm => "WaitAttachUserConfirm", + Self::SendChannelJoinRequest { .. } => "SendChannelJoinRequest", + Self::WaitChannelJoinConfirm { .. } => "WaitChannelJoinConfirm", + Self::AllJoined { .. } => "AllJoined", + } + } + + fn is_terminal(&self) -> bool { + matches!(self, Self::AllJoined { .. }) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct ChannelConnectionSequence { + pub state: ChannelConnectionState, + pub channel_ids: Option>, +} + +impl ChannelConnectionSequence { + pub fn new(io_channel_id: u16, channel_ids: Vec) -> Self { + let mut channel_ids: HashSet = channel_ids.into_iter().collect(); + + // I/O channel ID must be joined as well. + channel_ids.insert(io_channel_id); + + Self { + state: ChannelConnectionState::SendErectDomainRequest, + channel_ids: Some(channel_ids), + } + } + + pub fn skip_channel_join() -> Self { + Self { + state: ChannelConnectionState::SendErectDomainRequest, + channel_ids: None, + } + } +} + +impl Sequence for ChannelConnectionSequence { + fn next_pdu_hint(&self) -> Option<&dyn PduHint> { + match self.state { + ChannelConnectionState::Consumed => None, + ChannelConnectionState::SendErectDomainRequest => None, + ChannelConnectionState::SendAttachUserRequest => None, + ChannelConnectionState::WaitAttachUserConfirm => Some(&ironrdp_pdu::X224_HINT), + ChannelConnectionState::SendChannelJoinRequest { .. } => None, + ChannelConnectionState::WaitChannelJoinConfirm { .. } => Some(&ironrdp_pdu::X224_HINT), + ChannelConnectionState::AllJoined { .. } => None, + } + } + + fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult { + let (written, next_state) = match mem::take(&mut self.state) { + ChannelConnectionState::Consumed => { + return Err(general_err!( + "channel connection sequence state is consumed (this is a bug)", + )) + } + + ChannelConnectionState::SendErectDomainRequest => { + let erect_domain_request = mcs::ErectDomainPdu { + sub_height: 0, + sub_interval: 0, + }; + + debug!(message = ?erect_domain_request, "Send"); + + let written = + ironrdp_core::encode_buf(&X224(erect_domain_request), output).map_err(ConnectorError::encode)?; + + ( + Written::from_size(written)?, + ChannelConnectionState::SendAttachUserRequest, + ) + } + + ChannelConnectionState::SendAttachUserRequest => { + let attach_user_request = mcs::AttachUserRequest; + + debug!(message = ?attach_user_request, "Send"); + + let written = + ironrdp_core::encode_buf(&X224(attach_user_request), output).map_err(ConnectorError::encode)?; + + ( + Written::from_size(written)?, + ChannelConnectionState::WaitAttachUserConfirm, + ) + } + + ChannelConnectionState::WaitAttachUserConfirm => { + let attach_user_confirm = ironrdp_core::decode::>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + + let user_channel_id = attach_user_confirm.initiator_id; + + debug!(message = ?attach_user_confirm, user_channel_id, "Received"); + + let next = match self.channel_ids.take() { + Some(mut channel_ids) => { + // User channel ID must also be joined. + channel_ids.insert(user_channel_id); + + ChannelConnectionState::SendChannelJoinRequest { + user_channel_id, + join_channel_ids: channel_ids, + } + } + + None => ChannelConnectionState::AllJoined { user_channel_id }, + }; + + (Written::Nothing, next) + } + + // Send all the join requests in a single batch. + // > RDP 4.0, 5.0, 5.1, 5.2, 6.0, 6.1, 7.0, 7.1, 8.0, 10.2, 10.3, + // > 10.4, and 10.5 clients send a Channel Join Request to the server only after the + // > Channel Join Confirm for a previously sent request has been received. RDP 8.1, + // > 10.0, and 10.1 clients send all of the Channel Join Requests to the server in a + // > single batch to minimize the overall connection sequence time. + ChannelConnectionState::SendChannelJoinRequest { + user_channel_id, + join_channel_ids, + } => { + let mut total_written: usize = 0; + + debug_assert!(!join_channel_ids.is_empty()); + + for channel_id in join_channel_ids.iter().copied() { + let channel_join_request = mcs::ChannelJoinRequest { + initiator_id: user_channel_id, + channel_id, + }; + + debug!(message = ?channel_join_request, "Send"); + + let written = ironrdp_core::encode_buf(&X224(channel_join_request), output) + .map_err(ConnectorError::encode)?; + + total_written = total_written.checked_add(written).expect("small join request PDUs"); + } + + ( + Written::from_size(total_written)?, + ChannelConnectionState::WaitChannelJoinConfirm { + user_channel_id, + remaining_channel_ids: join_channel_ids, + }, + ) + } + + ChannelConnectionState::WaitChannelJoinConfirm { + user_channel_id, + mut remaining_channel_ids, + } => { + let channel_join_confirm = ironrdp_core::decode::>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + + debug!(message = ?channel_join_confirm, "Received"); + + if channel_join_confirm.initiator_id != user_channel_id { + warn!( + channel_join_confirm.initiator_id, + user_channel_id, "Inconsistent initiator ID for MCS Channel Join Confirm", + ) + } + + let is_expected = remaining_channel_ids.remove(&channel_join_confirm.requested_channel_id); + + if !is_expected { + return Err(reason_err!( + "ChannelJoinConfirm", + "unexpected requested_channel_id in MCS Channel Join Confirm: got {}, expected one of: {:?}", + channel_join_confirm.requested_channel_id, + remaining_channel_ids, + )); + } + + if channel_join_confirm.requested_channel_id != channel_join_confirm.channel_id { + // We could handle that gracefully by updating the StaticChannelSet, but it doesn’t seem to ever happen. + return Err(reason_err!( + "ChannelJoinConfirm", + "a channel was joined with a different channel ID than requested: requested {}, got {}", + channel_join_confirm.requested_channel_id, + channel_join_confirm.channel_id, + )); + } + + let next_state = if remaining_channel_ids.is_empty() { + ChannelConnectionState::AllJoined { user_channel_id } + } else { + ChannelConnectionState::WaitChannelJoinConfirm { + user_channel_id, + remaining_channel_ids, + } + }; + + (Written::Nothing, next_state) + } + + ChannelConnectionState::AllJoined { .. } => return Err(general_err!("all channels are already joined")), + }; + + self.state = next_state; + + Ok(written) + } + + fn state(&self) -> &dyn State { + &self.state + } +} diff --git a/crates/ironrdp-connector/src/connection.rs b/crates/ironrdp-connector/src/connection.rs new file mode 100644 index 00000000..3018b1f7 --- /dev/null +++ b/crates/ironrdp-connector/src/connection.rs @@ -0,0 +1,799 @@ +use core::mem; +use core::net::SocketAddr; +use std::borrow::Cow; +use std::sync::Arc; + +use ironrdp_core::{decode, encode_vec, Encode, WriteBuf}; +use ironrdp_pdu::x224::X224; +use ironrdp_pdu::{gcc, mcs, nego, rdp, PduHint}; +use ironrdp_svc::{StaticChannelSet, StaticVirtualChannel, SvcClientProcessor}; +use tracing::{debug, error, info, warn}; + +use crate::channel_connection::{ChannelConnectionSequence, ChannelConnectionState}; +use crate::connection_activation::{ConnectionActivationSequence, ConnectionActivationState}; +use crate::license_exchange::{LicenseExchangeSequence, NoopLicenseCache}; +use crate::{ + encode_x224_packet, general_err, reason_err, Config, ConnectorError, ConnectorErrorExt as _, ConnectorErrorKind, + ConnectorResult, DesktopSize, NegotiationFailure, Sequence, State, Written, +}; + +#[derive(Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct ConnectionResult { + pub io_channel_id: u16, + pub user_channel_id: u16, + pub static_channels: StaticChannelSet, + pub desktop_size: DesktopSize, + pub enable_server_pointer: bool, + pub pointer_software_rendering: bool, + pub connection_activation: ConnectionActivationSequence, +} + +#[derive(Default, Debug)] +#[non_exhaustive] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum ClientConnectorState { + #[default] + Consumed, + + ConnectionInitiationSendRequest, + ConnectionInitiationWaitConfirm { + requested_protocol: nego::SecurityProtocol, + }, + EnhancedSecurityUpgrade { + selected_protocol: nego::SecurityProtocol, + }, + Credssp { + selected_protocol: nego::SecurityProtocol, + }, + BasicSettingsExchangeSendInitial { + selected_protocol: nego::SecurityProtocol, + }, + BasicSettingsExchangeWaitResponse { + connect_initial: mcs::ConnectInitial, + }, + ChannelConnection { + io_channel_id: u16, + channel_connection: ChannelConnectionSequence, + }, + SecureSettingsExchange { + io_channel_id: u16, + user_channel_id: u16, + }, + ConnectTimeAutoDetection { + io_channel_id: u16, + user_channel_id: u16, + }, + LicensingExchange { + io_channel_id: u16, + user_channel_id: u16, + license_exchange: LicenseExchangeSequence, + }, + MultitransportBootstrapping { + io_channel_id: u16, + user_channel_id: u16, + }, + CapabilitiesExchange { + connection_activation: ConnectionActivationSequence, + }, + ConnectionFinalization { + connection_activation: ConnectionActivationSequence, + }, + Connected { + result: ConnectionResult, + }, +} + +impl State for ClientConnectorState { + fn name(&self) -> &'static str { + match self { + Self::Consumed => "Consumed", + Self::ConnectionInitiationSendRequest => "ConnectionInitiationSendRequest", + Self::ConnectionInitiationWaitConfirm { .. } => "ConnectionInitiationWaitResponse", + Self::EnhancedSecurityUpgrade { .. } => "EnhancedSecurityUpgrade", + Self::Credssp { .. } => "Credssp", + Self::BasicSettingsExchangeSendInitial { .. } => "BasicSettingsExchangeSendInitial", + Self::BasicSettingsExchangeWaitResponse { .. } => "BasicSettingsExchangeWaitResponse", + Self::ChannelConnection { .. } => "ChannelConnection", + Self::SecureSettingsExchange { .. } => "SecureSettingsExchange", + Self::ConnectTimeAutoDetection { .. } => "ConnectTimeAutoDetection", + Self::LicensingExchange { .. } => "LicensingExchange", + Self::MultitransportBootstrapping { .. } => "MultitransportBootstrapping", + Self::CapabilitiesExchange { + connection_activation, .. + } => connection_activation.state().name(), + Self::ConnectionFinalization { + connection_activation, .. + } => connection_activation.state().name(), + Self::Connected { .. } => "Connected", + } + } + + fn is_terminal(&self) -> bool { + matches!(self, Self::Connected { .. }) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct ClientConnector { + pub config: Config, + pub state: ClientConnectorState, + /// The client address to be used in the Client Info PDU. + pub client_addr: SocketAddr, + pub static_channels: StaticChannelSet, +} + +impl ClientConnector { + pub fn new(config: Config, client_addr: SocketAddr) -> Self { + Self { + config, + state: ClientConnectorState::ConnectionInitiationSendRequest, + client_addr, + static_channels: StaticChannelSet::new(), + } + } + + #[must_use] + pub fn with_static_channel(mut self, channel: T) -> Self + where + T: SvcClientProcessor + 'static, + { + self.static_channels.insert(channel); + self + } + + pub fn attach_static_channel(&mut self, channel: T) + where + T: SvcClientProcessor + 'static, + { + self.static_channels.insert(channel); + } + + pub fn get_static_channel_processor(&mut self) -> Option<&T> + where + T: SvcClientProcessor + 'static, + { + self.static_channels + .get_by_type::() + .and_then(|channel| channel.channel_processor_downcast_ref()) + } + + pub fn get_static_channel_processor_mut(&mut self) -> Option<&mut T> + where + T: SvcClientProcessor + 'static, + { + self.static_channels + .get_by_type_mut::() + .and_then(|channel| channel.channel_processor_downcast_mut()) + } + + pub fn should_perform_security_upgrade(&self) -> bool { + matches!(self.state, ClientConnectorState::EnhancedSecurityUpgrade { .. }) + } + + /// # Panics + /// + /// Panics if state is not [ClientConnectorState::EnhancedSecurityUpgrade]. + pub fn mark_security_upgrade_as_done(&mut self) { + assert!(self.should_perform_security_upgrade()); + self.step(&[], &mut WriteBuf::new()).expect("transition to next state"); + debug_assert!(!self.should_perform_security_upgrade()); + } + + pub fn should_perform_credssp(&self) -> bool { + matches!(self.state, ClientConnectorState::Credssp { .. }) + } + + /// # Panics + /// + /// Panics if state is not [ClientConnectorState::Credssp]. + pub fn mark_credssp_as_done(&mut self) { + assert!(self.should_perform_credssp()); + let res = self.step(&[], &mut WriteBuf::new()).expect("transition to next state"); + debug_assert!(!self.should_perform_credssp()); + assert_eq!(res, Written::Nothing); + } +} + +impl Sequence for ClientConnector { + fn next_pdu_hint(&self) -> Option<&dyn PduHint> { + match &self.state { + ClientConnectorState::Consumed => None, + ClientConnectorState::ConnectionInitiationSendRequest => None, + ClientConnectorState::ConnectionInitiationWaitConfirm { .. } => Some(&ironrdp_pdu::X224_HINT), + ClientConnectorState::EnhancedSecurityUpgrade { .. } => None, + ClientConnectorState::Credssp { .. } => None, + ClientConnectorState::BasicSettingsExchangeSendInitial { .. } => None, + ClientConnectorState::BasicSettingsExchangeWaitResponse { .. } => Some(&ironrdp_pdu::X224_HINT), + ClientConnectorState::ChannelConnection { channel_connection, .. } => channel_connection.next_pdu_hint(), + ClientConnectorState::SecureSettingsExchange { .. } => None, + ClientConnectorState::ConnectTimeAutoDetection { .. } => None, + ClientConnectorState::LicensingExchange { license_exchange, .. } => license_exchange.next_pdu_hint(), + ClientConnectorState::MultitransportBootstrapping { .. } => None, + ClientConnectorState::CapabilitiesExchange { + connection_activation, .. + } => connection_activation.next_pdu_hint(), + ClientConnectorState::ConnectionFinalization { + connection_activation, .. + } => connection_activation.next_pdu_hint(), + ClientConnectorState::Connected { .. } => None, + } + } + + fn state(&self) -> &dyn State { + &self.state + } + + fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult { + let (written, next_state) = match mem::take(&mut self.state) { + // Invalid state + ClientConnectorState::Consumed => { + return Err(general_err!("connector sequence state is consumed (this is a bug)",)) + } + + //== Connection Initiation ==// + // Exchange supported security protocols and a few other connection flags. + ClientConnectorState::ConnectionInitiationSendRequest => { + debug!("Connection Initiation"); + + let mut security_protocol = nego::SecurityProtocol::empty(); + + if self.config.enable_tls { + security_protocol.insert(nego::SecurityProtocol::SSL); + } + + if self.config.enable_credssp { + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/902b090b-9cb3-4efc-92bf-ee13373371e3 + // The spec is stating that `PROTOCOL_SSL` "SHOULD" also be set when using `PROTOCOL_HYBRID`. + // > PROTOCOL_HYBRID (0x00000002) + // > Credential Security Support Provider protocol (CredSSP) (section 5.4.5.2). + // > If this flag is set, then the PROTOCOL_SSL (0x00000001) flag SHOULD also be set + // > because Transport Layer Security (TLS) is a subset of CredSSP. + // However, crucially, it’s not strictly required (not "MUST"). + // In fact, we purposefully choose to not set `PROTOCOL_SSL` unless `enable_winlogon` is `true`. + // This tells the server that we are not going to accept downgrading NLA to TLS security. + security_protocol.insert(nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX); + } + + if security_protocol.is_standard_rdp_security() { + return Err(reason_err!("Initiation", "standard RDP security is not supported",)); + } + + let connection_request = nego::ConnectionRequest { + nego_data: self.config.request_data.clone().or_else(|| { + self.config + .credentials + .username() + .map(|username| nego::NegoRequestData::cookie(username.to_owned())) + }), + flags: nego::RequestFlags::empty(), + protocol: security_protocol, + }; + + debug!(message = ?connection_request, "Send"); + + let written = + ironrdp_core::encode_buf(&X224(connection_request), output).map_err(ConnectorError::encode)?; + + ( + Written::from_size(written)?, + ClientConnectorState::ConnectionInitiationWaitConfirm { + requested_protocol: security_protocol, + }, + ) + } + ClientConnectorState::ConnectionInitiationWaitConfirm { requested_protocol } => { + let connection_confirm = decode::>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + + debug!(message = ?connection_confirm, "Received"); + + let (flags, selected_protocol) = match connection_confirm { + nego::ConnectionConfirm::Response { flags, protocol } => (flags, protocol), + nego::ConnectionConfirm::Failure { code } => { + error!(?code, "Received connection failure code"); + return Err(ConnectorError::new( + "negotiation failure", + ConnectorErrorKind::Negotiation(NegotiationFailure::from(code)), + )); + } + }; + + info!(?selected_protocol, ?flags, "Server confirmed connection"); + + if !selected_protocol.intersects(requested_protocol) { + return Err(reason_err!( + "Initiation", + "client advertised {requested_protocol}, but server selected {selected_protocol}", + )); + } + + ( + Written::Nothing, + ClientConnectorState::EnhancedSecurityUpgrade { selected_protocol }, + ) + } + + //== Upgrade to Enhanced RDP Security ==// + // NOTE: we assume the selected protocol is never the standard RDP security (RC4). + // User code should match this variant and perform the appropriate upgrade (TLS handshake, etc). + ClientConnectorState::EnhancedSecurityUpgrade { selected_protocol } => { + let next_state = if selected_protocol + .intersects(nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX) + { + debug!("Begin NLA using CredSSP"); + ClientConnectorState::Credssp { selected_protocol } + } else { + debug!("CredSSP is disabled, skipping NLA"); + ClientConnectorState::BasicSettingsExchangeSendInitial { selected_protocol } + }; + + (Written::Nothing, next_state) + } + + //== CredSSP ==// + ClientConnectorState::Credssp { selected_protocol } => ( + Written::Nothing, + ClientConnectorState::BasicSettingsExchangeSendInitial { selected_protocol }, + ), + + //== Basic Settings Exchange ==// + // Exchange basic settings including Core Data, Security Data and Network Data. + ClientConnectorState::BasicSettingsExchangeSendInitial { selected_protocol } => { + debug!("Basic Settings Exchange"); + + let client_gcc_blocks = + create_gcc_blocks(&self.config, selected_protocol, self.static_channels.values())?; + + let connect_initial = + mcs::ConnectInitial::with_gcc_blocks(client_gcc_blocks).map_err(ConnectorError::decode)?; + + debug!(message = ?connect_initial, "Send"); + + let written = encode_x224_packet(&connect_initial, output)?; + + ( + Written::from_size(written)?, + ClientConnectorState::BasicSettingsExchangeWaitResponse { connect_initial }, + ) + } + ClientConnectorState::BasicSettingsExchangeWaitResponse { connect_initial } => { + let x224_payload = decode::>>(input) + .map_err(ConnectorError::decode) + .map(|p| p.0)?; + let connect_response = + decode::(x224_payload.data.as_ref()).map_err(ConnectorError::decode)?; + + debug!(message = ?connect_response, "Received"); + + let client_gcc_blocks = connect_initial.conference_create_request.gcc_blocks(); + + let server_gcc_blocks = connect_response.conference_create_response.into_gcc_blocks(); + + if client_gcc_blocks.security == gcc::ClientSecurityData::no_security() + && server_gcc_blocks.security != gcc::ServerSecurityData::no_security() + { + return Err(general_err!("can't satisfy server security settings")); + } + + if server_gcc_blocks.message_channel.is_some() { + warn!("Unexpected ServerMessageChannelData GCC block (not supported)"); + } + + if server_gcc_blocks.multi_transport_channel.is_some() { + warn!("Unexpected MultiTransportChannelData GCC block (not supported)"); + } + + let static_channel_ids = server_gcc_blocks.network.channel_ids; + let io_channel_id = server_gcc_blocks.network.io_channel; + + debug!(?static_channel_ids, io_channel_id); + + let zipped: Vec<_> = self + .static_channels + .type_ids() + .zip(static_channel_ids.iter().copied()) + .collect(); + + zipped.into_iter().for_each(|(channel, channel_id)| { + self.static_channels.attach_channel_id(channel, channel_id); + }); + + let skip_channel_join = server_gcc_blocks + .core + .optional_data + .early_capability_flags + .is_some_and(|c| c.contains(gcc::ServerEarlyCapabilityFlags::SKIP_CHANNELJOIN_SUPPORTED)); + + ( + Written::Nothing, + ClientConnectorState::ChannelConnection { + io_channel_id, + channel_connection: if skip_channel_join { + ChannelConnectionSequence::skip_channel_join() + } else { + ChannelConnectionSequence::new(io_channel_id, static_channel_ids) + }, + }, + ) + } + + //== Channel Connection ==// + // Connect every individual channel. + ClientConnectorState::ChannelConnection { + io_channel_id, + mut channel_connection, + } => { + debug!("Channel Connection"); + let written = channel_connection.step(input, output)?; + + let next_state = if let ChannelConnectionState::AllJoined { user_channel_id } = channel_connection.state + { + debug_assert!(channel_connection.state.is_terminal()); + + ClientConnectorState::SecureSettingsExchange { + io_channel_id, + user_channel_id, + } + } else { + ClientConnectorState::ChannelConnection { + io_channel_id, + channel_connection, + } + }; + + (written, next_state) + } + + //== RDP Security Commencement ==// + // When using standard RDP security (RC4), a Security Exchange PDU is sent at this point. + // However, IronRDP does not support this unsecure security protocol (purposefully) and + // this part of the sequence is not implemented. + //==============================// + + //== Secure Settings Exchange ==// + // Send Client Info PDU (information about supported types of compression, username, password, etc). + ClientConnectorState::SecureSettingsExchange { + io_channel_id, + user_channel_id, + } => { + debug!("Secure Settings Exchange"); + + let client_info = create_client_info_pdu(&self.config, &self.client_addr); + + debug!(message = ?client_info, "Send"); + + let written = encode_send_data_request(user_channel_id, io_channel_id, &client_info, output)?; + + ( + Written::from_size(written)?, + ClientConnectorState::ConnectTimeAutoDetection { + io_channel_id, + user_channel_id, + }, + ) + } + + //== Optional Connect-Time Auto-Detection ==// + // NOTE: IronRDP is not expecting the Auto-Detect Request PDU from server. + ClientConnectorState::ConnectTimeAutoDetection { + io_channel_id, + user_channel_id, + } => ( + Written::Nothing, + ClientConnectorState::LicensingExchange { + io_channel_id, + user_channel_id, + license_exchange: LicenseExchangeSequence::new( + io_channel_id, + self.config.credentials.username().unwrap_or("").to_owned(), + self.config.domain.clone(), + self.config.hardware_id.unwrap_or_default(), + self.config + .license_cache + .clone() + .unwrap_or_else(|| Arc::new(NoopLicenseCache)), + ), + }, + ), + + //== Licensing ==// + // Server is sending information regarding licensing. + // Typically useful when support for more than two simultaneous connections is required (terminal server). + ClientConnectorState::LicensingExchange { + io_channel_id, + user_channel_id, + mut license_exchange, + } => { + debug!("Licensing Exchange"); + + let written = license_exchange.step(input, output)?; + + let next_state = if license_exchange.state.is_terminal() { + ClientConnectorState::MultitransportBootstrapping { + io_channel_id, + user_channel_id, + } + } else { + ClientConnectorState::LicensingExchange { + io_channel_id, + user_channel_id, + license_exchange, + } + }; + + (written, next_state) + } + + //== Optional Multitransport Bootstrapping ==// + // NOTE: our implementation is not expecting the Auto-Detect Request PDU from server + ClientConnectorState::MultitransportBootstrapping { + io_channel_id, + user_channel_id, + } => ( + Written::Nothing, + ClientConnectorState::CapabilitiesExchange { + connection_activation: ConnectionActivationSequence::new( + self.config.clone(), + io_channel_id, + user_channel_id, + ), + }, + ), + + //== Capabilities Exchange ==/ + // The server sends the set of capabilities it supports to the client. + ClientConnectorState::CapabilitiesExchange { + mut connection_activation, + } => { + let written = connection_activation.step(input, output)?; + match connection_activation.connection_activation_state() { + ConnectionActivationState::ConnectionFinalization { .. } => ( + written, + ClientConnectorState::ConnectionFinalization { connection_activation }, + ), + _ => return Err(general_err!("invalid state (this is a bug)")), + } + } + + //== Connection Finalization ==// + // Client and server exchange a few PDUs in order to finalize the connection. + // Client may send PDUs one after the other without waiting for a response in order to speed up the process. + ClientConnectorState::ConnectionFinalization { + mut connection_activation, + } => { + let written = connection_activation.step(input, output)?; + + let next_state = if !connection_activation.connection_activation_state().is_terminal() { + ClientConnectorState::ConnectionFinalization { connection_activation } + } else { + match connection_activation.connection_activation_state() { + ConnectionActivationState::Finalized { + io_channel_id, + user_channel_id, + desktop_size, + enable_server_pointer, + pointer_software_rendering, + } => ClientConnectorState::Connected { + result: ConnectionResult { + io_channel_id, + user_channel_id, + static_channels: mem::take(&mut self.static_channels), + desktop_size, + enable_server_pointer, + pointer_software_rendering, + connection_activation, + }, + }, + _ => return Err(general_err!("invalid state (this is a bug)")), + } + }; + + (written, next_state) + } + + //== Connected ==// + // The client connector job is done. + ClientConnectorState::Connected { .. } => return Err(general_err!("already connected")), + }; + + self.state = next_state; + + Ok(written) + } +} + +pub fn encode_send_data_request( + initiator_id: u16, + channel_id: u16, + user_msg: &T, + buf: &mut WriteBuf, +) -> ConnectorResult { + let user_data = encode_vec(user_msg).map_err(ConnectorError::encode)?; + + let pdu = mcs::SendDataRequest { + initiator_id, + channel_id, + user_data: Cow::Owned(user_data), + }; + + let written = ironrdp_core::encode_buf(&X224(pdu), buf).map_err(ConnectorError::encode)?; + + Ok(written) +} + +#[expect(single_use_lifetimes)] // anonymous lifetimes in `impl Trait` are unstable +fn create_gcc_blocks<'a>( + config: &Config, + selected_protocol: nego::SecurityProtocol, + static_channels: impl Iterator, +) -> ConnectorResult { + use ironrdp_pdu::gcc::{ + ClientCoreData, ClientCoreOptionalData, ClientEarlyCapabilityFlags, ClientGccBlocks, ClientNetworkData, + ClientSecurityData, ColorDepth, ConnectionType, EncryptionMethod, HighColorDepth, MonitorOrientation, + RdpVersion, SecureAccessSequence, SupportedColorDepths, + }; + + let max_color_depth = config.bitmap.as_ref().map(|bitmap| bitmap.color_depth).unwrap_or(32); + + let supported_color_depths = match max_color_depth { + 15 => SupportedColorDepths::BPP15, + 16 => SupportedColorDepths::BPP16, + 24 => SupportedColorDepths::BPP24, + 32 => SupportedColorDepths::BPP32 | SupportedColorDepths::BPP16, + _ => { + return Err(reason_err!( + "create gcc blocks", + "unsupported color depth: {max_color_depth}" + )) + } + }; + + let channels = static_channels + .map(ironrdp_svc::make_channel_definition) + .collect::>(); + + Ok(ClientGccBlocks { + core: ClientCoreData { + version: RdpVersion::V5_PLUS, + desktop_width: config.desktop_size.width, + desktop_height: config.desktop_size.height, + color_depth: ColorDepth::Bpp8, // ignored because we use the optional core data below + sec_access_sequence: SecureAccessSequence::Del, + keyboard_layout: config.keyboard_layout, + client_build: config.client_build, + client_name: config.client_name.clone(), + keyboard_type: config.keyboard_type, + keyboard_subtype: config.keyboard_subtype, + keyboard_functional_keys_count: config.keyboard_functional_keys_count, + ime_file_name: config.ime_file_name.clone(), + optional_data: ClientCoreOptionalData { + post_beta2_color_depth: Some(ColorDepth::Bpp8), // ignored because we set high_color_depth + client_product_id: Some(1), + serial_number: Some(0), + high_color_depth: Some(HighColorDepth::Bpp24), + supported_color_depths: Some(supported_color_depths), + early_capability_flags: { + let mut early_capability_flags = ClientEarlyCapabilityFlags::VALID_CONNECTION_TYPE + | ClientEarlyCapabilityFlags::SUPPORT_ERR_INFO_PDU + | ClientEarlyCapabilityFlags::STRONG_ASYMMETRIC_KEYS + | ClientEarlyCapabilityFlags::SUPPORT_SKIP_CHANNELJOIN; + + // TODO(#136): support for ClientEarlyCapabilityFlags::SUPPORT_STATUS_INFO_PDU + + if max_color_depth == 32 { + early_capability_flags |= ClientEarlyCapabilityFlags::WANT_32_BPP_SESSION; + } + + Some(early_capability_flags) + }, + dig_product_id: Some(config.dig_product_id.clone()), + connection_type: Some(ConnectionType::Lan), + server_selected_protocol: Some(selected_protocol), + desktop_physical_width: Some(0), // 0 per FreeRDP + desktop_physical_height: Some(0), // 0 per FreeRDP + desktop_orientation: if config.desktop_size.width > config.desktop_size.height { + Some(MonitorOrientation::Landscape.as_u16()) + } else { + Some(MonitorOrientation::Portrait.as_u16()) + }, + desktop_scale_factor: Some(config.desktop_scale_factor), + device_scale_factor: if config.desktop_scale_factor >= 100 && config.desktop_scale_factor <= 500 { + Some(100) + } else { + Some(0) + }, + }, + }, + security: ClientSecurityData { + encryption_methods: EncryptionMethod::empty(), + ext_encryption_methods: 0, + }, + network: if channels.is_empty() { + None + } else { + Some(ClientNetworkData { channels }) + }, + // TODO(#139): support for Some(ClientClusterData { flags: RedirectionFlags::REDIRECTION_SUPPORTED, redirection_version: RedirectionVersion::V4, redirected_session_id: 0, }), + cluster: None, + monitor: None, + // TODO(#140): support for Client Message Channel Data (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/f50e791c-de03-4b25-b17e-e914c9020bc3) + message_channel: None, + // TODO(#140): support for Some(MultiTransportChannelData { flags: MultiTransportFlags::empty(), }) + multi_transport_channel: None, + monitor_extended: None, + }) +} + +fn create_client_info_pdu(config: &Config, client_addr: &SocketAddr) -> rdp::ClientInfoPdu { + use ironrdp_pdu::rdp::client_info::{ + AddressFamily, ClientInfo, ClientInfoFlags, CompressionType, Credentials, ExtendedClientInfo, + ExtendedClientOptionalInfo, + }; + use ironrdp_pdu::rdp::headers::{BasicSecurityHeader, BasicSecurityHeaderFlags}; + use ironrdp_pdu::rdp::ClientInfoPdu; + + let security_header = BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::INFO_PKT, + }; + + // Default flags for all sessions + let mut flags = ClientInfoFlags::MOUSE + | ClientInfoFlags::MOUSE_HAS_WHEEL + | ClientInfoFlags::UNICODE + | ClientInfoFlags::DISABLE_CTRL_ALT_DEL + | ClientInfoFlags::LOGON_NOTIFY + | ClientInfoFlags::LOGON_ERRORS + | ClientInfoFlags::VIDEO_DISABLE + | ClientInfoFlags::ENABLE_WINDOWS_KEY + | ClientInfoFlags::MAXIMIZE_SHELL; + + if config.autologon { + flags |= ClientInfoFlags::AUTOLOGON; + } + + if let crate::Credentials::SmartCard { .. } = &config.credentials { + flags |= ClientInfoFlags::PASSWORD_IS_SC_PIN; + } + + if !config.enable_audio_playback { + flags |= ClientInfoFlags::NO_AUDIO_PLAYBACK; + } + + let client_info = ClientInfo { + credentials: Credentials { + username: config.credentials.username().unwrap_or("").to_owned(), + password: config.credentials.secret().to_owned(), + domain: config.domain.clone(), + }, + code_page: 0, // ignored if the keyboardLayout field of the Client Core Data is set to zero + flags, + compression_type: CompressionType::K8, // ignored if ClientInfoFlags::COMPRESSION is not set + alternate_shell: String::new(), + work_dir: String::new(), + extra_info: ExtendedClientInfo { + address_family: match client_addr { + SocketAddr::V4(_) => AddressFamily::INET, + SocketAddr::V6(_) => AddressFamily::INET_6, + }, + address: client_addr.ip().to_string(), + dir: config.client_dir.clone(), + optional_data: ExtendedClientOptionalInfo::builder() + .timezone(config.timezone_info.clone()) + .session_id(0) + .performance_flags(config.performance_flags) + .build(), + }, + }; + + ClientInfoPdu { + security_header, + client_info, + } +} diff --git a/crates/ironrdp-connector/src/connection_activation.rs b/crates/ironrdp-connector/src/connection_activation.rs new file mode 100644 index 00000000..110ba29d --- /dev/null +++ b/crates/ironrdp-connector/src/connection_activation.rs @@ -0,0 +1,401 @@ +use core::mem; + +use ironrdp_pdu::rdp; +use ironrdp_pdu::rdp::capability_sets::CapabilitySet; +use tracing::{debug, warn}; + +use crate::{ + general_err, legacy, Config, ConnectionFinalizationSequence, ConnectorResult, DesktopSize, Sequence, State, Written, +}; + +/// Represents the Capability Exchange and Connection Finalization phases +/// of the connection sequence (section [1.3.1.1]). +/// +/// This is abstracted into its own struct to allow it to be used for the ordinary +/// RDP connection sequence [`ClientConnector`] that occurs for every RDP connection, +/// as well as the Deactivation-Reactivation Sequence ([1.3.1.3]) that occurs when +/// a [Server Deactivate All PDU] is received. +/// +/// [1.3.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/023f1e69-cfe8-4ee6-9ee0-7e759fb4e4ee +/// [1.3.1.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432 +/// [`ClientConnector`]: crate::ClientConnector +/// [Server Deactivate All PDU]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/8a29971a-df3c-48da-add2-8ed9a05edc89 +#[derive(Debug, Clone)] +pub struct ConnectionActivationSequence { + state: ConnectionActivationState, + config: Config, +} + +impl ConnectionActivationSequence { + pub fn new(config: Config, io_channel_id: u16, user_channel_id: u16) -> Self { + Self { + state: ConnectionActivationState::CapabilitiesExchange { + io_channel_id, + user_channel_id, + }, + config, + } + } + + /// Returns the current state as a district type, rather than `&dyn State` provided by [`Self::state`]. + pub fn connection_activation_state(&self) -> ConnectionActivationState { + self.state + } + + #[must_use] + pub fn reset_clone(&self) -> Self { + self.clone().reset() + } + + fn reset(mut self) -> Self { + match &self.state { + ConnectionActivationState::CapabilitiesExchange { + io_channel_id, + user_channel_id, + } + | ConnectionActivationState::ConnectionFinalization { + io_channel_id, + user_channel_id, + .. + } + | ConnectionActivationState::Finalized { + io_channel_id, + user_channel_id, + .. + } => { + self.state = ConnectionActivationState::CapabilitiesExchange { + io_channel_id: *io_channel_id, + user_channel_id: *user_channel_id, + }; + + self + } + ConnectionActivationState::Consumed => self, + } + } +} + +impl Sequence for ConnectionActivationSequence { + fn next_pdu_hint(&self) -> Option<&dyn ironrdp_pdu::PduHint> { + match &self.state { + ConnectionActivationState::Consumed => None, + ConnectionActivationState::Finalized { .. } => None, + ConnectionActivationState::CapabilitiesExchange { .. } => Some(&ironrdp_pdu::X224_HINT), + ConnectionActivationState::ConnectionFinalization { + connection_finalization, + .. + } => connection_finalization.next_pdu_hint(), + } + } + + fn state(&self) -> &dyn State { + &self.state + } + + fn step(&mut self, input: &[u8], output: &mut ironrdp_core::WriteBuf) -> ConnectorResult { + let (written, next_state) = match mem::take(&mut self.state) { + ConnectionActivationState::Consumed | ConnectionActivationState::Finalized { .. } => { + return Err(general_err!( + "connector sequence state is finalized or consumed (this is a bug)" + )); + } + ConnectionActivationState::CapabilitiesExchange { + io_channel_id, + user_channel_id, + } => { + debug!("Capabilities Exchange"); + + let send_data_indication_ctx = legacy::decode_send_data_indication(input)?; + let share_control_ctx = legacy::decode_share_control(send_data_indication_ctx)?; + + debug!(message = ?share_control_ctx.pdu, "Received"); + + if share_control_ctx.channel_id != io_channel_id { + warn!( + io_channel_id, + share_control_ctx.channel_id, "Unexpected channel ID for received Share Control Pdu" + ); + } + + let capability_sets = if let rdp::headers::ShareControlPdu::ServerDemandActive(server_demand_active) = + share_control_ctx.pdu + { + server_demand_active.pdu.capability_sets + } else { + return Err(general_err!( + "unexpected Share Control Pdu (expected ServerDemandActive)", + )); + }; + + for c in &capability_sets { + if let CapabilitySet::General(g) = c { + if g.protocol_version != rdp::capability_sets::PROTOCOL_VER { + warn!(version = g.protocol_version, "Unexpected protocol version"); + } + break; + } + } + + // At this point we have already sent a requested desktop size to the server -- either as a part of the + // [`TS_UD_CS_CORE`] (on initial connection) or the [`DISPLAYCONTROL_MONITOR_LAYOUT`] (on resize event). + // + // The server is therefore responding with a desktop size here, which will be close to the requested size but + // may be slightly different due to server-side constraints. We should use this negotiated size for the rest of + // the session. + // + // [TS_UD_CS_CORE]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/00f1da4a-ee9c-421a-852f-c19f92343d73 + // [DISPLAYCONTROL_MONITOR_LAYOUT]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + let desktop_size = capability_sets + .iter() + .find_map(|c| match c { + CapabilitySet::Bitmap(b) => Some(DesktopSize { + width: b.desktop_width, + height: b.desktop_height, + }), + _ => None, + }) + .unwrap_or(DesktopSize { + width: self.config.desktop_size.width, + height: self.config.desktop_size.height, + }); + + let client_confirm_active = rdp::headers::ShareControlPdu::ClientConfirmActive( + create_client_confirm_active(&self.config, capability_sets, desktop_size), + ); + + debug!(message = ?client_confirm_active, "Send"); + + let written = legacy::encode_share_control( + user_channel_id, + io_channel_id, + share_control_ctx.share_id, + client_confirm_active, + output, + )?; + + ( + Written::from_size(written)?, + ConnectionActivationState::ConnectionFinalization { + io_channel_id, + user_channel_id, + desktop_size, + connection_finalization: ConnectionFinalizationSequence::new(io_channel_id, user_channel_id), + }, + ) + } + ConnectionActivationState::ConnectionFinalization { + io_channel_id, + user_channel_id, + desktop_size, + mut connection_finalization, + } => { + debug!("Connection Finalization"); + + let written = connection_finalization.step(input, output)?; + + let next_state = if !connection_finalization.state.is_terminal() { + ConnectionActivationState::ConnectionFinalization { + io_channel_id, + user_channel_id, + desktop_size, + connection_finalization, + } + } else { + ConnectionActivationState::Finalized { + io_channel_id, + user_channel_id, + desktop_size, + enable_server_pointer: self.config.enable_server_pointer, + pointer_software_rendering: self.config.pointer_software_rendering, + } + }; + + (written, next_state) + } + }; + + self.state = next_state; + + Ok(written) + } +} + +#[derive(Default, Debug, Copy, Clone)] +pub enum ConnectionActivationState { + #[default] + Consumed, + CapabilitiesExchange { + io_channel_id: u16, + user_channel_id: u16, + }, + ConnectionFinalization { + io_channel_id: u16, + user_channel_id: u16, + desktop_size: DesktopSize, + connection_finalization: ConnectionFinalizationSequence, + }, + Finalized { + io_channel_id: u16, + user_channel_id: u16, + desktop_size: DesktopSize, + enable_server_pointer: bool, + pointer_software_rendering: bool, + }, +} + +impl State for ConnectionActivationState { + fn name(&self) -> &'static str { + match self { + ConnectionActivationState::Consumed => "Consumed", + ConnectionActivationState::CapabilitiesExchange { .. } => "CapabilitiesExchange", + ConnectionActivationState::ConnectionFinalization { .. } => "ConnectionFinalization", + ConnectionActivationState::Finalized { .. } => "Finalized", + } + } + + fn is_terminal(&self) -> bool { + matches!(self, ConnectionActivationState::Finalized { .. }) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +const DEFAULT_POINTER_CACHE_SIZE: u16 = 32; + +fn create_client_confirm_active( + config: &Config, + mut server_capability_sets: Vec, + desktop_size: DesktopSize, +) -> rdp::capability_sets::ClientConfirmActive { + use ironrdp_pdu::rdp::capability_sets::{ + client_codecs_capabilities, Bitmap, BitmapCache, BitmapDrawingFlags, Brush, CacheDefinition, CacheEntry, + ClientConfirmActive, CmdFlags, DemandActive, FrameAcknowledge, General, GeneralExtraFlags, GlyphCache, + GlyphSupportLevel, Input, InputFlags, LargePointer, LargePointerSupportFlags, MultifragmentUpdate, + OffscreenBitmapCache, Order, OrderFlags, OrderSupportExFlags, Pointer, Sound, SoundFlags, SupportLevel, + SurfaceCommands, VirtualChannel, VirtualChannelFlags, BITMAP_CACHE_ENTRIES_NUM, GLYPH_CACHE_NUM, + SERVER_CHANNEL_ID, + }; + + server_capability_sets.retain(|capability_set| matches!(capability_set, CapabilitySet::MultiFragmentUpdate(_))); + + let lossy_bitmap_compression = config + .bitmap + .as_ref() + .map(|bitmap| bitmap.lossy_compression) + .unwrap_or(false); + + let drawing_flags = if lossy_bitmap_compression { + BitmapDrawingFlags::ALLOW_SKIP_ALPHA + | BitmapDrawingFlags::ALLOW_DYNAMIC_COLOR_FIDELITY + | BitmapDrawingFlags::ALLOW_COLOR_SUBSAMPLING + } else { + BitmapDrawingFlags::ALLOW_SKIP_ALPHA + }; + + server_capability_sets.extend_from_slice(&[ + CapabilitySet::General(General { + major_platform_type: config.platform, + extra_flags: GeneralExtraFlags::FASTPATH_OUTPUT_SUPPORTED | GeneralExtraFlags::NO_BITMAP_COMPRESSION_HDR, + ..Default::default() + }), + CapabilitySet::Bitmap(Bitmap { + pref_bits_per_pix: 32, + desktop_width: desktop_size.width, + desktop_height: desktop_size.height, + // This is required to be true in order for the Microsoft::Windows::RDS::DisplayControl DVC to work. + desktop_resize_flag: true, + drawing_flags, + }), + CapabilitySet::Order(Order::new( + OrderFlags::NEGOTIATE_ORDER_SUPPORT | OrderFlags::ZERO_BOUNDS_DELTAS_SUPPORT, + OrderSupportExFlags::empty(), + 0, + 0, + )), + CapabilitySet::BitmapCache(BitmapCache { + caches: [CacheEntry { + entries: 0, + max_cell_size: 0, + }; BITMAP_CACHE_ENTRIES_NUM], + }), + CapabilitySet::Input(Input { + input_flags: InputFlags::all(), + keyboard_layout: 0, + keyboard_type: Some(config.keyboard_type), + keyboard_subtype: config.keyboard_subtype, + keyboard_function_key: config.keyboard_functional_keys_count, + keyboard_ime_filename: config.ime_file_name.clone(), + }), + CapabilitySet::Pointer(Pointer { + // Pointer cache should be set to non-zero value to enable client-side pointer rendering. + color_pointer_cache_size: DEFAULT_POINTER_CACHE_SIZE, + pointer_cache_size: DEFAULT_POINTER_CACHE_SIZE, + }), + CapabilitySet::Brush(Brush { + support_level: SupportLevel::Default, + }), + CapabilitySet::GlyphCache(GlyphCache { + glyph_cache: [CacheDefinition { + entries: 0, + max_cell_size: 0, + }; GLYPH_CACHE_NUM], + frag_cache: CacheDefinition { + entries: 0, + max_cell_size: 0, + }, + glyph_support_level: GlyphSupportLevel::None, + }), + CapabilitySet::OffscreenBitmapCache(OffscreenBitmapCache { + is_supported: false, + cache_size: 0, + cache_entries: 0, + }), + CapabilitySet::VirtualChannel(VirtualChannel { + flags: VirtualChannelFlags::NO_COMPRESSION, + chunk_size: Some(0), // ignored + }), + CapabilitySet::Sound(Sound { + flags: SoundFlags::empty(), + }), + CapabilitySet::LargePointer(LargePointer { + // Setting `LargePointerSupportFlags::UP_TO_384X384_PIXELS` allows server to send + // `TS_FP_LARGEPOINTERATTRIBUTE` update messages, which are required for client-side + // rendering of pointers bigger than 96x96 pixels. + // `LargePointerSupportFlags::UP_TO_96X96_PIXELS` is needed for proper cursor behavior + // in Windows 2019 and older + flags: LargePointerSupportFlags::UP_TO_96X96_PIXELS | LargePointerSupportFlags::UP_TO_384X384_PIXELS, + }), + CapabilitySet::SurfaceCommands(SurfaceCommands { + flags: CmdFlags::SET_SURFACE_BITS | CmdFlags::STREAM_SURFACE_BITS | CmdFlags::FRAME_MARKER, + }), + CapabilitySet::BitmapCodecs(match config.bitmap.as_ref().map(|b| b.codecs.clone()) { + Some(codecs) => codecs, + None => client_codecs_capabilities(&[]).expect("can't panic for &[]"), + }), + CapabilitySet::FrameAcknowledge(FrameAcknowledge { + // FIXME(#447): Revert this to 2 per FreeRDP. + // This is a temporary hack to fix a resize bug, see: + // https://github.com/Devolutions/IronRDP/issues/447 + max_unacknowledged_frame_count: 20, + }), + ]); + + if !server_capability_sets + .iter() + .any(|c| matches!(&c, CapabilitySet::MultiFragmentUpdate(_))) + { + server_capability_sets.push(CapabilitySet::MultiFragmentUpdate(MultifragmentUpdate { + max_request_size: 8 * 1024 * 1024, // 8 MB + })); + } + + ClientConfirmActive { + originator_id: SERVER_CHANNEL_ID, + pdu: DemandActive { + source_descriptor: "IRONRDP".to_owned(), + capability_sets: server_capability_sets, + }, + } +} diff --git a/crates/ironrdp-connector/src/connection_finalization.rs b/crates/ironrdp-connector/src/connection_finalization.rs new file mode 100644 index 00000000..cb626d99 --- /dev/null +++ b/crates/ironrdp-connector/src/connection_finalization.rs @@ -0,0 +1,237 @@ +use core::mem; + +use ironrdp_core::WriteBuf; +use ironrdp_pdu::rdp::capability_sets::SERVER_CHANNEL_ID; +use ironrdp_pdu::rdp::headers::ShareDataPdu; +use ironrdp_pdu::rdp::{finalization_messages, server_error_info}; +use ironrdp_pdu::PduHint; +use tracing::{debug, warn}; + +use crate::{general_err, legacy, reason_err, ConnectorResult, Sequence, State, Written}; + +#[derive(Default, Debug, Copy, Clone)] +#[non_exhaustive] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum ConnectionFinalizationState { + #[default] + Consumed, + + SendSynchronize, + SendControlCooperate, + SendRequestControl, + SendFontList, + + WaitForResponse, + + Finished, +} + +impl State for ConnectionFinalizationState { + fn name(&self) -> &'static str { + match self { + Self::Consumed => "Consumed", + Self::SendSynchronize => "SendSynchronize", + Self::SendControlCooperate => "SendControlCooperate", + Self::SendRequestControl => "SendRequestControl", + Self::SendFontList => "SendFontList", + Self::WaitForResponse => "WaitForResponse", + Self::Finished => "Finished", + } + } + + fn is_terminal(&self) -> bool { + matches!(self, Self::Finished) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct ConnectionFinalizationSequence { + pub state: ConnectionFinalizationState, + pub io_channel_id: u16, + pub user_channel_id: u16, +} + +impl ConnectionFinalizationSequence { + pub fn new(io_channel_id: u16, user_channel_id: u16) -> Self { + Self { + state: ConnectionFinalizationState::SendSynchronize, + io_channel_id, + user_channel_id, + } + } +} + +impl Sequence for ConnectionFinalizationSequence { + fn next_pdu_hint(&self) -> Option<&dyn PduHint> { + match self.state { + ConnectionFinalizationState::Consumed => None, + ConnectionFinalizationState::SendSynchronize => None, + ConnectionFinalizationState::SendControlCooperate => None, + ConnectionFinalizationState::SendRequestControl => None, + ConnectionFinalizationState::SendFontList => None, + ConnectionFinalizationState::WaitForResponse => Some(&ironrdp_pdu::X224_HINT), + ConnectionFinalizationState::Finished => None, + } + } + + fn state(&self) -> &dyn State { + &self.state + } + + fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult { + let (written, next_state) = match mem::take(&mut self.state) { + ConnectionFinalizationState::Consumed => { + return Err(general_err!( + "connection finalization sequence state is consumed (this is a bug)", + )) + } + + ConnectionFinalizationState::SendSynchronize => { + let message = ShareDataPdu::Synchronize(finalization_messages::SynchronizePdu { + target_user_id: self.user_channel_id, + }); + + debug!(?message, "Send"); + + let written = legacy::encode_share_data(self.user_channel_id, self.io_channel_id, 0, message, output)?; + + ( + Written::from_size(written)?, + ConnectionFinalizationState::SendControlCooperate, + ) + } + + ConnectionFinalizationState::SendControlCooperate => { + let message = ShareDataPdu::Control(finalization_messages::ControlPdu { + action: finalization_messages::ControlAction::Cooperate, + grant_id: 0, + control_id: 0, + }); + + debug!(?message, "Send"); + + let written = legacy::encode_share_data(self.user_channel_id, self.io_channel_id, 0, message, output)?; + + ( + Written::from_size(written)?, + ConnectionFinalizationState::SendRequestControl, + ) + } + + ConnectionFinalizationState::SendRequestControl => { + let message = ShareDataPdu::Control(finalization_messages::ControlPdu { + action: finalization_messages::ControlAction::RequestControl, + grant_id: 0, + control_id: 0, + }); + + debug!(?message, "Send"); + + let written = legacy::encode_share_data(self.user_channel_id, self.io_channel_id, 0, message, output)?; + + (Written::from_size(written)?, ConnectionFinalizationState::SendFontList) + } + + ConnectionFinalizationState::SendFontList => { + let message = ShareDataPdu::FontList(finalization_messages::FontPdu::default()); + + debug!(?message, "Send"); + + let written = legacy::encode_share_data(self.user_channel_id, self.io_channel_id, 0, message, output)?; + + ( + Written::from_size(written)?, + ConnectionFinalizationState::WaitForResponse, + ) + } + + ConnectionFinalizationState::WaitForResponse => { + let ctx = legacy::decode_send_data_indication(input)?; + let ctx = legacy::decode_share_data(ctx)?; + + debug!(message = ?ctx.pdu, "Received"); + + let next_state = match ctx.pdu { + ShareDataPdu::Synchronize(_) => { + debug!("Server Synchronize"); + ConnectionFinalizationState::WaitForResponse + } + ShareDataPdu::Control(control_pdu) => { + match control_pdu.action { + finalization_messages::ControlAction::Cooperate => { + if control_pdu.grant_id == 0 && control_pdu.control_id == 0 { + debug!("Server Control (Cooperate)"); + } else { + warn!( + control_pdu.grant_id, + control_pdu.control_id, + user_channel_id = self.user_channel_id, + "Server Control (Cooperate) has non-zero grant_id or control_id", + ); + } + ConnectionFinalizationState::WaitForResponse + } + finalization_messages::ControlAction::GrantedControl => { + debug!( + control_pdu.grant_id, + control_pdu.control_id, + user_channel_id = self.user_channel_id, + SERVER_CHANNEL_ID + ); + + if control_pdu.grant_id != self.user_channel_id { + warn!("Server Control (Granted Control) had invalid grant_id, expected {}, but got {}", self.user_channel_id, control_pdu.grant_id); + } + + if control_pdu.control_id != u32::from(SERVER_CHANNEL_ID) { + warn!("Server Control (Granted Control) had invalid control_id, expected {}, but got {}", SERVER_CHANNEL_ID, control_pdu.control_id); + } + + ConnectionFinalizationState::WaitForResponse + } + _ => return Err(general_err!("unexpected control action")), + } + } + ShareDataPdu::ServerSetErrorInfo(server_error_info::ServerSetErrorInfoPdu(error_info)) => { + match error_info { + server_error_info::ErrorInfo::ProtocolIndependentCode( + server_error_info::ProtocolIndependentCode::None, + ) => ConnectionFinalizationState::WaitForResponse, + _ => { + return Err(reason_err!( + "ServerSetErrorInfo", + "server returned error info: {}", + error_info.description() + )); + } + } + } + ShareDataPdu::FontMap(_) => { + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/023f1e69-cfe8-4ee6-9ee0-7e759fb4e4ee + // + // Once the client has sent the Confirm Active PDU, it can start + // sending mouse and keyboard input to the server, and upon receipt + // of the Font List PDU the server can start sending graphics + // output to the client. + + ConnectionFinalizationState::Finished + } + _ => return Err(general_err!("unexpected server message")), + }; + + (Written::Nothing, next_state) + } + + ConnectionFinalizationState::Finished => return Err(general_err!("finalization already finished")), + }; + + self.state = next_state; + + Ok(written) + } +} diff --git a/crates/ironrdp-connector/src/credssp.rs b/crates/ironrdp-connector/src/credssp.rs new file mode 100644 index 00000000..866ac508 --- /dev/null +++ b/crates/ironrdp-connector/src/credssp.rs @@ -0,0 +1,277 @@ +use ironrdp_core::{other_err, WriteBuf}; +use ironrdp_pdu::{nego, PduHint}; +use picky::key::PrivateKey; +use picky_asn1_x509::{oids, Certificate, ExtensionView, GeneralName}; +use sspi::credssp::{self, ClientState, CredSspClient}; +use sspi::generator::{Generator, NetworkRequest}; +use sspi::negotiate::ProtocolConfig; +use sspi::Secret; +use sspi::Username; +use tracing::debug; + +use crate::{ + custom_err, general_err, ConnectorError, ConnectorErrorKind, ConnectorResult, Credentials, ServerName, Written, +}; + +#[derive(Debug, Clone, Default)] +pub struct KerberosConfig { + pub kdc_proxy_url: Option, + pub hostname: Option, +} + +impl KerberosConfig { + pub fn new(kdc_proxy_url: Option, hostname: Option) -> ConnectorResult { + let kdc_proxy_url = kdc_proxy_url + .map(|url| url::Url::parse(&url)) + .transpose() + .map_err(|e| custom_err!("invalid KDC URL", e))?; + Ok(Self { + kdc_proxy_url, + hostname, + }) + } +} + +impl From for sspi::KerberosConfig { + fn from(val: KerberosConfig) -> Self { + sspi::KerberosConfig { + kdc_url: val.kdc_proxy_url, + client_computer_name: val.hostname, + } + } +} + +#[derive(Clone, Copy, Debug)] +struct CredsspTsRequestHint; + +const CREDSSP_TS_REQUEST_HINT: CredsspTsRequestHint = CredsspTsRequestHint; + +impl PduHint for CredsspTsRequestHint { + fn find_size(&self, bytes: &[u8]) -> ironrdp_core::DecodeResult> { + match credssp::TsRequest::read_length(bytes) { + Ok(length) => Ok(Some((true, length))), + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None), + Err(e) => Err(other_err!("CredsspTsRequestHint", source: e)), + } + } +} + +#[derive(Clone, Copy, Debug)] +struct CredsspEarlyUserAuthResultHint; + +const CREDSSP_EARLY_USER_AUTH_RESULT_HINT: CredsspEarlyUserAuthResultHint = CredsspEarlyUserAuthResultHint; + +impl PduHint for CredsspEarlyUserAuthResultHint { + fn find_size(&self, _: &[u8]) -> ironrdp_core::DecodeResult> { + Ok(Some((true, credssp::EARLY_USER_AUTH_RESULT_PDU_SIZE))) + } +} + +pub type CredsspProcessGenerator<'a> = Generator<'a, NetworkRequest, sspi::Result>, sspi::Result>; + +#[derive(Debug)] +pub struct CredsspSequence { + client: CredSspClient, + state: CredsspState, + selected_protocol: nego::SecurityProtocol, +} + +#[derive(Debug, PartialEq)] +pub(crate) enum CredsspState { + Ongoing, + EarlyUserAuthResult, + Finished, +} + +impl CredsspSequence { + pub fn next_pdu_hint(&self) -> Option<&dyn PduHint> { + match self.state { + CredsspState::Ongoing => Some(&CREDSSP_TS_REQUEST_HINT), + CredsspState::EarlyUserAuthResult => Some(&CREDSSP_EARLY_USER_AUTH_RESULT_HINT), + CredsspState::Finished => None, + } + } + + /// `server_name` must be the actual target server hostname (as opposed to the proxy) + pub fn init( + credentials: Credentials, + domain: Option<&str>, + protocol: nego::SecurityProtocol, + server_name: ServerName, + server_public_key: Vec, + kerberos_config: Option, + ) -> ConnectorResult<(Self, credssp::TsRequest)> { + let credentials: sspi::Credentials = match &credentials { + Credentials::UsernamePassword { username, password } => { + let username = Username::new(username, domain).map_err(|e| custom_err!("invalid username", e))?; + + sspi::AuthIdentity { + username, + password: password.to_owned().into(), + } + .into() + } + Credentials::SmartCard { pin, config } => match config { + Some(config) => { + let cert: Certificate = picky_asn1_der::from_bytes(&config.certificate) + .map_err(|_e| general_err!("can't parse certificate"))?; + let key = PrivateKey::from_pkcs1(&config.private_key) + .map_err(|_e| general_err!("can't parse private key"))?; + let identity = sspi::SmartCardIdentity { + username: extract_user_principal_name(&cert) + .or_else(|| extract_user_name(&cert)) + .unwrap_or_default(), + certificate: cert, + reader_name: config.reader_name.clone(), + card_name: None, + container_name: Some(config.container_name.clone()), + csp_name: config.csp_name.clone(), + pin: pin.as_bytes().to_vec().into(), + private_key: Some(key.into()), + scard_type: sspi::SmartCardType::Emulated { + scard_pin: Secret::new(pin.as_bytes().to_vec()), + }, + }; + sspi::Credentials::SmartCard(Box::new(identity)) + } + None => { + return Err(general_err!("smart card configuration missing")); + } + }, + }; + + let server_name = server_name.into_inner(); + + let service_principal_name = format!("TERMSRV/{}", &server_name); + + let credssp_config: Box; + if let Some(ref krb_config) = kerberos_config { + credssp_config = Box::new(Into::::into(krb_config.clone())); + } else { + credssp_config = Box::::default(); + } + debug!(?credssp_config); + + let client = CredSspClient::new( + server_public_key, + credentials, + credssp::CredSspMode::WithCredentials, + credssp::ClientMode::Negotiate(sspi::NegotiateConfig { + protocol_config: credssp_config, + package_list: None, + client_computer_name: server_name, + }), + service_principal_name, + ) + .map_err(|e| ConnectorError::new("CredSSP", ConnectorErrorKind::Credssp(e)))?; + + let sequence = Self { + client, + state: CredsspState::Ongoing, + selected_protocol: protocol, + }; + + let initial_request = credssp::TsRequest::default(); + + Ok((sequence, initial_request)) + } + + /// Returns Some(ts_request) when a TS request is received from server, + /// and None when an early user auth result PDU is received instead. + pub fn decode_server_message(&mut self, input: &[u8]) -> ConnectorResult> { + match self.state { + CredsspState::Ongoing => { + let message = credssp::TsRequest::from_buffer(input).map_err(|e| custom_err!("TsRequest", e))?; + debug!(?message, "Received"); + Ok(Some(message)) + } + CredsspState::EarlyUserAuthResult => { + let early_user_auth_result = credssp::EarlyUserAuthResult::from_buffer(input) + .map_err(|e| custom_err!("EarlyUserAuthResult", e))?; + + debug!(message = ?early_user_auth_result, "Received"); + + match early_user_auth_result { + credssp::EarlyUserAuthResult::Success => { + self.state = CredsspState::Finished; + Ok(None) + } + credssp::EarlyUserAuthResult::AccessDenied => { + Err(ConnectorError::new("CredSSP", ConnectorErrorKind::AccessDenied)) + } + } + } + _ => Err(general_err!( + "attempted to feed server request to CredSSP sequence in an unexpected state" + )), + } + } + + pub fn process_ts_request(&mut self, request: credssp::TsRequest) -> CredsspProcessGenerator<'_> { + self.client.process(request) + } + + pub fn handle_process_result(&mut self, result: ClientState, output: &mut WriteBuf) -> ConnectorResult { + let (size, next_state) = match self.state { + CredsspState::Ongoing => { + let (ts_request_from_client, next_state) = match result { + ClientState::ReplyNeeded(ts_request) => (ts_request, CredsspState::Ongoing), + ClientState::FinalMessage(ts_request) => ( + ts_request, + if self.selected_protocol.contains(nego::SecurityProtocol::HYBRID_EX) { + CredsspState::EarlyUserAuthResult + } else { + CredsspState::Finished + }, + ), + }; + + debug!(message = ?ts_request_from_client, "Send"); + + let written = write_credssp_request(ts_request_from_client, output)?; + + Ok((Written::from_size(written)?, next_state)) + } + CredsspState::EarlyUserAuthResult => Ok((Written::Nothing, CredsspState::Finished)), + CredsspState::Finished => Err(general_err!("CredSSP sequence is already done")), + }?; + + self.state = next_state; + + Ok(size) + } +} + +fn extract_user_name(cert: &Certificate) -> Option { + cert.tbs_certificate.subject.find_common_name().map(ToString::to_string) +} + +fn extract_user_principal_name(cert: &Certificate) -> Option { + cert.extensions() + .iter() + .find(|ext| ext.extn_id().0 == oids::subject_alternative_name()) + .iter() + .flat_map(|ext| match ext.extn_value() { + ExtensionView::SubjectAltName(names) => names.0, + _ => vec![], + }) + .find_map(|name| match name { + GeneralName::OtherName(name) if name.type_id.0 == oids::user_principal_name() => Some(name.value), + _ => None, + }) + .and_then(|asn1| picky_asn1_der::from_bytes(&asn1.0 .0).ok()) +} + +fn write_credssp_request(ts_request: credssp::TsRequest, output: &mut WriteBuf) -> ConnectorResult { + let length = usize::from(ts_request.buffer_len()); + + let unfilled_buffer = output.unfilled_to(length); + + ts_request + .encode_ts_request(unfilled_buffer) + .map_err(|e| custom_err!("TsRequest", e))?; + + output.advance(length); + + Ok(length) +} diff --git a/crates/ironrdp-connector/src/legacy.rs b/crates/ironrdp-connector/src/legacy.rs new file mode 100644 index 00000000..e71fa97e --- /dev/null +++ b/crates/ironrdp-connector/src/legacy.rs @@ -0,0 +1,192 @@ +use std::borrow::Cow; + +use ironrdp_core::{decode, encode_vec, Decode, Encode, WriteBuf}; +use ironrdp_pdu::rdp; +use ironrdp_pdu::rdp::headers::ServerDeactivateAll; +use ironrdp_pdu::x224::X224; + +use crate::{general_err, reason_err, ConnectorError, ConnectorErrorExt as _, ConnectorResult}; + +pub fn encode_send_data_request( + initiator_id: u16, + channel_id: u16, + user_msg: &T, + buf: &mut WriteBuf, +) -> ConnectorResult +where + T: Encode, +{ + let user_data = encode_vec(user_msg).map_err(ConnectorError::encode)?; + + let pdu = ironrdp_pdu::mcs::SendDataRequest { + initiator_id, + channel_id, + user_data: Cow::Owned(user_data), + }; + + let written = ironrdp_core::encode_buf(&X224(pdu), buf).map_err(ConnectorError::encode)?; + + Ok(written) +} + +#[derive(Debug, Clone, Copy)] +pub struct SendDataIndicationCtx<'a> { + pub initiator_id: u16, + pub channel_id: u16, + pub user_data: &'a [u8], +} + +impl<'a> SendDataIndicationCtx<'a> { + pub fn decode_user_data<'de, T>(&self) -> ConnectorResult + where + T: Decode<'de>, + 'a: 'de, + { + let msg = decode::(self.user_data).map_err(ConnectorError::decode)?; + Ok(msg) + } +} + +pub fn decode_send_data_indication(src: &[u8]) -> ConnectorResult> { + use ironrdp_pdu::mcs::McsMessage; + + let mcs_msg = decode::>>(src).map_err(ConnectorError::decode)?; + + match mcs_msg.0 { + McsMessage::SendDataIndication(msg) => { + let Cow::Borrowed(user_data) = msg.user_data else { + unreachable!() + }; + + Ok(SendDataIndicationCtx { + initiator_id: msg.initiator_id, + channel_id: msg.channel_id, + user_data, + }) + } + McsMessage::DisconnectProviderUltimatum(msg) => Err(reason_err!( + "decode_send_data_indication", + "received disconnect provider ultimatum: {:?}", + msg.reason + )), + _ => Err(reason_err!( + "decode_send_data_indication", + "unexpected MCS message: {}", + ironrdp_core::name(&mcs_msg) + )), + } +} + +pub fn encode_share_control( + initiator_id: u16, + channel_id: u16, + share_id: u32, + pdu: rdp::headers::ShareControlPdu, + buf: &mut WriteBuf, +) -> ConnectorResult { + let pdu_source = initiator_id; + + let share_control_header = rdp::headers::ShareControlHeader { + share_control_pdu: pdu, + pdu_source, + share_id, + }; + + encode_send_data_request(initiator_id, channel_id, &share_control_header, buf) +} + +#[derive(Debug, Clone)] +pub struct ShareControlCtx { + pub initiator_id: u16, + pub channel_id: u16, + pub share_id: u32, + pub pdu_source: u16, + pub pdu: rdp::headers::ShareControlPdu, +} + +pub fn decode_share_control(ctx: SendDataIndicationCtx<'_>) -> ConnectorResult { + let user_msg = ctx.decode_user_data::()?; + + Ok(ShareControlCtx { + initiator_id: ctx.initiator_id, + channel_id: ctx.channel_id, + share_id: user_msg.share_id, + pdu_source: user_msg.pdu_source, + pdu: user_msg.share_control_pdu, + }) +} + +pub fn encode_share_data( + initiator_id: u16, + channel_id: u16, + share_id: u32, + pdu: rdp::headers::ShareDataPdu, + buf: &mut WriteBuf, +) -> ConnectorResult { + let share_data_header = rdp::headers::ShareDataHeader { + share_data_pdu: pdu, + stream_priority: rdp::headers::StreamPriority::Medium, + compression_flags: rdp::headers::CompressionFlags::empty(), + compression_type: rdp::client_info::CompressionType::K8, // ignored if CompressionFlags::empty() + }; + + let share_control_pdu = rdp::headers::ShareControlPdu::Data(share_data_header); + + encode_share_control(initiator_id, channel_id, share_id, share_control_pdu, buf) +} + +#[derive(Debug, Clone)] +pub struct ShareDataCtx { + pub initiator_id: u16, + pub channel_id: u16, + pub share_id: u32, + pub pdu_source: u16, + pub pdu: rdp::headers::ShareDataPdu, +} + +pub fn decode_share_data(ctx: SendDataIndicationCtx<'_>) -> ConnectorResult { + let ctx = decode_share_control(ctx)?; + + let rdp::headers::ShareControlPdu::Data(share_data_header) = ctx.pdu else { + return Err(general_err!( + "received unexpected Share Control Pdu (expected Share Data Header)" + )); + }; + + Ok(ShareDataCtx { + initiator_id: ctx.initiator_id, + channel_id: ctx.channel_id, + share_id: ctx.share_id, + pdu_source: ctx.pdu_source, + pdu: share_data_header.share_data_pdu, + }) +} + +pub enum IoChannelPdu { + Data(ShareDataCtx), + DeactivateAll(ServerDeactivateAll), +} + +pub fn decode_io_channel(ctx: SendDataIndicationCtx<'_>) -> ConnectorResult { + let ctx = decode_share_control(ctx)?; + + match ctx.pdu { + rdp::headers::ShareControlPdu::ServerDeactivateAll(deactivate_all) => { + Ok(IoChannelPdu::DeactivateAll(deactivate_all)) + } + rdp::headers::ShareControlPdu::Data(share_data_header) => { + let share_data_ctx = ShareDataCtx { + initiator_id: ctx.initiator_id, + channel_id: ctx.channel_id, + share_id: ctx.share_id, + pdu_source: ctx.pdu_source, + pdu: share_data_header.share_data_pdu, + }; + + Ok(IoChannelPdu::Data(share_data_ctx)) + } + _ => Err(general_err!( + "received unexpected Share Control Pdu (expected Share Data Header or Server Deactivate All)" + )), + } +} diff --git a/crates/ironrdp-connector/src/lib.rs b/crates/ironrdp-connector/src/lib.rs new file mode 100644 index 00000000..1e1a7402 --- /dev/null +++ b/crates/ironrdp-connector/src/lib.rs @@ -0,0 +1,435 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +mod macros; + +pub mod legacy; + +mod channel_connection; +mod connection; +pub mod connection_activation; +mod connection_finalization; +pub mod credssp; +mod license_exchange; +mod server_name; + +use core::any::Any; +use core::fmt; +use std::sync::Arc; + +use ironrdp_core::{encode_buf, encode_vec, Encode, WriteBuf}; +use ironrdp_pdu::nego::NegoRequestData; +use ironrdp_pdu::rdp::capability_sets::{self, BitmapCodecs}; +use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; +use ironrdp_pdu::x224::X224; +use ironrdp_pdu::{gcc, x224, PduHint}; +pub use sspi; + +pub use self::channel_connection::{ChannelConnectionSequence, ChannelConnectionState}; +pub use self::connection::{encode_send_data_request, ClientConnector, ClientConnectorState, ConnectionResult}; +pub use self::connection_finalization::{ConnectionFinalizationSequence, ConnectionFinalizationState}; +pub use self::license_exchange::{LicenseExchangeSequence, LicenseExchangeState}; +pub use self::server_name::ServerName; +pub use crate::license_exchange::LicenseCache; + +/// Provides user-friendly error messages for RDP negotiation failures +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct NegotiationFailure(ironrdp_pdu::nego::FailureCode); + +impl NegotiationFailure { + pub fn code(self) -> ironrdp_pdu::nego::FailureCode { + self.0 + } +} + +impl core::error::Error for NegotiationFailure {} + +impl From for NegotiationFailure { + fn from(code: ironrdp_pdu::nego::FailureCode) -> Self { + Self(code) + } +} + +impl fmt::Display for NegotiationFailure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use ironrdp_pdu::nego::FailureCode; + + match self.0 { + FailureCode::SSL_REQUIRED_BY_SERVER => { + write!(f, "server requires Enhanced RDP Security with TLS or CredSSP") + } + FailureCode::SSL_NOT_ALLOWED_BY_SERVER => { + write!(f, "server only supports Standard RDP Security") + } + FailureCode::SSL_CERT_NOT_ON_SERVER => { + write!(f, "server lacks valid authentication certificate") + } + FailureCode::INCONSISTENT_FLAGS => { + write!(f, "inconsistent security protocol flags") + } + FailureCode::HYBRID_REQUIRED_BY_SERVER => { + write!(f, "server requires Enhanced RDP Security with CredSSP") + } + FailureCode::SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER => { + write!( + f, + "server requires Enhanced RDP Security with TLS and client certificate" + ) + } + _ => write!(f, "unknown negotiation failure (code: 0x{:08x})", u32::from(self.0)), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct DesktopSize { + pub width: u16, + pub height: u16, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct BitmapConfig { + pub lossy_compression: bool, + pub color_depth: u32, + pub codecs: BitmapCodecs, +} + +#[derive(Debug, Clone)] +pub struct SmartCardIdentity { + /// DER-encoded X509 certificate + pub certificate: Vec, + /// Smart card reader name + pub reader_name: String, + /// Smart card key container name + pub container_name: String, + /// Smart card CSP name + pub csp_name: String, + /// DER-encoded RSA 2048-bit private key + pub private_key: Vec, +} + +#[derive(Debug, Clone)] +pub enum Credentials { + UsernamePassword { + username: String, + password: String, + }, + SmartCard { + pin: String, + config: Option, + }, +} + +impl Credentials { + fn username(&self) -> Option<&str> { + match self { + Self::UsernamePassword { username, .. } => Some(username), + Self::SmartCard { .. } => None, // Username is ultimately provided by the smart card certificate. + } + } + + fn secret(&self) -> &str { + match self { + Self::UsernamePassword { password, .. } => password, + Self::SmartCard { pin, .. } => pin, + } + } +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct Config { + /// The initial desktop size to request + pub desktop_size: DesktopSize, + /// The initial desktop scale factor to request. + /// + /// This becomes the `desktop_scale_factor` in the [`TS_UD_CS_CORE`](gcc::ClientCoreOptionalData) structure. + pub desktop_scale_factor: u32, + /// TLS + Graphical login (legacy) + /// + /// Also called SSL or TLS security protocol. + /// The PROTOCOL_SSL flag will be set. + /// + /// When this security protocol is negotiated, the RDP server will show a graphical login screen. + /// For Windows, it means that the login subsystem (winlogon.exe) and the GDI graphics subsystem + /// will be initiated and the user will authenticate himself using LogonUI.exe, as if + /// using the physical machine directly. + /// + /// This security protocol is being phased out because it’s not great security-wise. + /// Indeed, the whole RDP connection sequence will be performed, allowing anyone to effectively + /// open a RDP session session with all static channels joined and active (e.g.: I/O, clipboard, + /// sound, drive redirection, etc). This exposes a wide attack surface with many impacts on both + /// the client and the server. + /// + /// - Man-in-the-middle (MITM) + /// - Server-side takeover + /// - Client-side file stealing + /// - Client-side takeover + /// + /// Recommended reads on this topic: + /// + /// - + /// - + /// - + /// - + /// + /// By setting this option to `false`, it’s possible to effectively enforce usage of NLA on client side. + pub enable_tls: bool, + /// TLS + Network Level Authentication (NLA) using CredSSP + /// + /// The PROTOCOL_HYBRID and PROTOCOL_HYBRID_EX flags will be set. + /// + /// NLA is allowing authentication to be performed before session establishment. + /// + /// This option includes the extended CredSSP early user authorization result PDU. + /// This PDU is used by the server to deny access before any credentials (except for the username) + /// have been submitted, e.g.: typically if the user does not have the necessary remote access + /// privileges. + /// + /// The attack surface is considerably reduced in comparison to the legacy "TLS" security protocol. + /// For this reason, it is recommended to set `enable_tls` to `false` when connecting to NLA-capable + /// computers. + #[doc(alias("enable_nla", "nla"))] + pub enable_credssp: bool, + pub credentials: Credentials, + pub domain: Option, + /// The build number of the client. + pub client_build: u32, + /// Name of the client computer + /// + /// The name will be truncated to the 15 first characters. + pub client_name: String, + pub keyboard_type: gcc::KeyboardType, + pub keyboard_subtype: u32, + pub keyboard_functional_keys_count: u32, + pub keyboard_layout: u32, + pub ime_file_name: String, + pub bitmap: Option, + pub dig_product_id: String, + pub client_dir: String, + pub platform: capability_sets::MajorPlatformType, + /// Unique identifier for the computer + /// + /// Each 32-bit integer contains client hardware-specific data helping the server uniquely identify the client. + pub hardware_id: Option<[u32; 4]>, + /// Optional data for the x224 connection request. + /// + /// Fallbacks to a sensible default depending on the provided credentials: + /// + /// - A cookie containing the username for a username/password. + /// - Nothing for a smart card. + pub request_data: Option, + /// If true, the INFO_AUTOLOGON flag is set in the [`ClientInfoPdu`](ironrdp_pdu::rdp::ClientInfoPdu) + pub autologon: bool, + /// If true, the INFO_NOAUDIOPLAYBACK flag is set in the [`ClientInfoPdu`](ironrdp_pdu::rdp::ClientInfoPdu) + pub enable_audio_playback: bool, + pub performance_flags: PerformanceFlags, + + pub license_cache: Option>, + + // For Timezone Redirection to sync the server's timezone with the client's. + pub timezone_info: TimezoneInfo, + + // FIXME(@CBenoit): these are client-only options, not part of the connector. + pub enable_server_pointer: bool, + pub pointer_software_rendering: bool, +} + +ironrdp_core::assert_impl!(Config: Send, Sync); + +pub trait State: Send + fmt::Debug + 'static { + fn name(&self) -> &'static str; + fn is_terminal(&self) -> bool; + fn as_any(&self) -> &dyn Any; +} + +ironrdp_core::assert_obj_safe!(State); + +pub fn state_downcast(state: &dyn State) -> Option<&T> { + state.as_any().downcast_ref() +} + +pub fn state_is(state: &dyn State) -> bool { + state.as_any().is::() +} + +impl State for () { + fn name(&self) -> &'static str { + "()" + } + + fn is_terminal(&self) -> bool { + true + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Written { + Nothing, + Size(core::num::NonZeroUsize), +} + +impl Written { + #[inline] + pub fn from_size(value: usize) -> ConnectorResult { + core::num::NonZeroUsize::new(value) + .map(Self::Size) + .ok_or_else(|| ConnectorError::general("invalid written length (can't be zero)")) + } + + #[inline] + pub fn is_nothing(self) -> bool { + matches!(self, Self::Nothing) + } + + #[inline] + pub fn size(self) -> Option { + if let Self::Size(size) = self { + Some(size.get()) + } else { + None + } + } +} + +pub trait Sequence: Send { + fn next_pdu_hint(&self) -> Option<&dyn PduHint>; + + fn state(&self) -> &dyn State; + + fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult; + + fn step_no_input(&mut self, output: &mut WriteBuf) -> ConnectorResult { + self.step(&[], output) + } +} + +ironrdp_core::assert_obj_safe!(Sequence); + +pub type ConnectorResult = Result; + +#[non_exhaustive] +#[derive(Debug)] +pub enum ConnectorErrorKind { + Encode(ironrdp_core::EncodeError), + Decode(ironrdp_core::DecodeError), + Credssp(sspi::Error), + Reason(String), + AccessDenied, + General, + Custom, + Negotiation(NegotiationFailure), +} + +impl fmt::Display for ConnectorErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + ConnectorErrorKind::Encode(_) => write!(f, "encode error"), + ConnectorErrorKind::Decode(_) => write!(f, "decode error"), + ConnectorErrorKind::Credssp(_) => write!(f, "CredSSP"), + ConnectorErrorKind::Reason(description) => write!(f, "reason: {description}"), + ConnectorErrorKind::AccessDenied => write!(f, "access denied"), + ConnectorErrorKind::General => write!(f, "general error"), + ConnectorErrorKind::Custom => write!(f, "custom error"), + ConnectorErrorKind::Negotiation(failure) => write!(f, "negotiation failure: {failure}"), + } + } +} + +impl core::error::Error for ConnectorErrorKind { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match &self { + ConnectorErrorKind::Encode(e) => Some(e), + ConnectorErrorKind::Decode(e) => Some(e), + ConnectorErrorKind::Credssp(e) => Some(e), + ConnectorErrorKind::Reason(_) => None, + ConnectorErrorKind::AccessDenied => None, + ConnectorErrorKind::Custom => None, + ConnectorErrorKind::General => None, + ConnectorErrorKind::Negotiation(failure) => Some(failure), + } + } +} + +pub type ConnectorError = ironrdp_error::Error; + +pub trait ConnectorErrorExt { + fn encode(error: ironrdp_core::EncodeError) -> Self; + fn decode(error: ironrdp_core::DecodeError) -> Self; + fn general(context: &'static str) -> Self; + fn reason(context: &'static str, reason: impl Into) -> Self; + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static; +} + +impl ConnectorErrorExt for ConnectorError { + fn encode(error: ironrdp_core::EncodeError) -> Self { + Self::new("encode error", ConnectorErrorKind::Encode(error)) + } + + fn decode(error: ironrdp_core::DecodeError) -> Self { + Self::new("decode error", ConnectorErrorKind::Decode(error)) + } + + fn general(context: &'static str) -> Self { + Self::new(context, ConnectorErrorKind::General) + } + + fn reason(context: &'static str, reason: impl Into) -> Self { + Self::new(context, ConnectorErrorKind::Reason(reason.into())) + } + + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static, + { + Self::new(context, ConnectorErrorKind::Custom).with_source(e) + } +} + +pub trait ConnectorResultExt { + #[must_use] + fn with_context(self, context: &'static str) -> Self; + #[must_use] + fn with_source(self, source: E) -> Self + where + E: core::error::Error + Sync + Send + 'static; +} + +impl ConnectorResultExt for ConnectorResult { + fn with_context(self, context: &'static str) -> Self { + self.map_err(|mut e| { + e.set_context(context); + e + }) + } + + fn with_source(self, source: E) -> Self + where + E: core::error::Error + Sync + Send + 'static, + { + self.map_err(|e| e.with_source(source)) + } +} + +pub fn encode_x224_packet(x224_msg: &T, buf: &mut WriteBuf) -> ConnectorResult +where + T: Encode, +{ + let x224_msg_buf = encode_vec(x224_msg).map_err(ConnectorError::encode)?; + + let pdu = x224::X224Data { + data: std::borrow::Cow::Owned(x224_msg_buf), + }; + + let written = encode_buf(&X224(pdu), buf).map_err(ConnectorError::encode)?; + + Ok(written) +} diff --git a/crates/ironrdp-connector/src/license_exchange.rs b/crates/ironrdp-connector/src/license_exchange.rs new file mode 100644 index 00000000..ae8b5d85 --- /dev/null +++ b/crates/ironrdp-connector/src/license_exchange.rs @@ -0,0 +1,365 @@ +use core::fmt::Debug; +use core::panic::RefUnwindSafe; +use core::{fmt, mem}; +use std::str; +use std::sync::Arc; + +use ironrdp_core::WriteBuf; +use ironrdp_pdu::rdp::server_license::{self, LicenseInformation, LicensePdu, ServerLicenseError}; +use ironrdp_pdu::PduHint; +use rand::RngCore as _; +use tracing::{debug, error, info, trace}; + +use super::{custom_err, general_err, legacy, ConnectorError, ConnectorErrorExt as _}; +use crate::{encode_send_data_request, ConnectorResult, ConnectorResultExt as _, Sequence, State, Written}; + +#[derive(Default, Debug)] +#[non_exhaustive] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum LicenseExchangeState { + #[default] + Consumed, + + NewLicenseRequest, + PlatformChallenge { + encryption_data: server_license::LicenseEncryptionData, + }, + UpgradeLicense { + encryption_data: server_license::LicenseEncryptionData, + }, + LicenseExchanged, +} + +impl State for LicenseExchangeState { + fn name(&self) -> &'static str { + match self { + Self::Consumed => "Consumed", + Self::NewLicenseRequest => "NewLicenseRequest", + Self::PlatformChallenge { .. } => "PlatformChallenge", + Self::UpgradeLicense { .. } => "UpgradeLicense", + Self::LicenseExchanged => "LicenseExchanged", + } + } + + fn is_terminal(&self) -> bool { + matches!(self, Self::LicenseExchanged) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +/// Client licensing sequence +/// +/// Implements the state machine described in MS-RDPELE, section [3.1.5.3.1] Client State Transition. +/// +/// [3.1.5.3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/8f9b860a-3687-401d-b3bc-7e9f5d4f7528 +#[derive(Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct LicenseExchangeSequence { + pub state: LicenseExchangeState, + pub io_channel_id: u16, + pub username: String, + pub domain: Option, + pub hardware_id: [u32; 4], + pub license_cache: Arc, +} + +// Use RefUnwindSafe so that types that embed LicenseCache remain UnwindSafe +pub trait LicenseCache: Sync + Send + Debug + RefUnwindSafe { + fn get_license(&self, license_info: LicenseInformation) -> ConnectorResult>>; + fn store_license(&self, license_info: LicenseInformation) -> ConnectorResult<()>; +} + +#[derive(Debug)] +pub(crate) struct NoopLicenseCache; + +impl LicenseCache for NoopLicenseCache { + fn get_license(&self, _license_info: LicenseInformation) -> ConnectorResult>> { + Ok(None) + } + + fn store_license(&self, _license_info: LicenseInformation) -> ConnectorResult<()> { + Ok(()) + } +} + +impl LicenseExchangeSequence { + pub fn new( + io_channel_id: u16, + username: String, + domain: Option, + hardware_id: [u32; 4], + license_cache: Arc, + ) -> Self { + Self { + state: LicenseExchangeState::NewLicenseRequest, + io_channel_id, + username, + domain, + hardware_id, + license_cache, + } + } +} + +impl Sequence for LicenseExchangeSequence { + fn next_pdu_hint(&self) -> Option<&dyn PduHint> { + match self.state { + LicenseExchangeState::Consumed => None, + LicenseExchangeState::NewLicenseRequest => Some(&ironrdp_pdu::X224_HINT), + LicenseExchangeState::PlatformChallenge { .. } => Some(&ironrdp_pdu::X224_HINT), + LicenseExchangeState::UpgradeLicense { .. } => Some(&ironrdp_pdu::X224_HINT), + LicenseExchangeState::LicenseExchanged => None, + } + } + + fn state(&self) -> &dyn State { + &self.state + } + + fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult { + let (written, next_state) = match mem::take(&mut self.state) { + LicenseExchangeState::Consumed => { + return Err(general_err!( + "license exchange sequence state is consumed (this is a bug)", + )) + } + + LicenseExchangeState::NewLicenseRequest => { + let send_data_indication_ctx = legacy::decode_send_data_indication(input)?; + let license_pdu = send_data_indication_ctx + .decode_user_data::() + .with_context("decode during LicenseExchangeState::NewLicenseRequest")?; + + match license_pdu { + LicensePdu::ServerLicenseRequest(license_request) => { + let mut rng = rand::rng(); + let mut client_random = [0u8; server_license::RANDOM_NUMBER_SIZE]; + rng.fill_bytes(&mut client_random); + + let mut premaster_secret = [0u8; server_license::PREMASTER_SECRET_SIZE]; + rng.fill_bytes(&mut premaster_secret); + + let license_info = license_request + .scope_list + .iter() + .filter_map(|scope| { + self.license_cache + .get_license(LicenseInformation { + version: license_request.product_info.version, + scope: scope.0.clone(), + company_name: license_request.product_info.company_name.clone(), + product_id: license_request.product_info.product_id.clone(), + license_info: vec![], + }) + .transpose() + }) + .next() + .transpose()?; + + if let Some(info) = license_info { + match server_license::ClientLicenseInfo::from_server_license_request( + &license_request, + &client_random, + &premaster_secret, + self.hardware_id, + info, + ) { + Ok((client_license_info, encryption_data)) => { + trace!(?encryption_data, "Successfully generated Client License Info"); + trace!(message = ?client_license_info, "Send"); + + let written = encode_send_data_request::( + send_data_indication_ctx.initiator_id, + send_data_indication_ctx.channel_id, + &client_license_info.into(), + output, + )?; + + trace!(?written, "Written ClientLicenseInfo"); + + ( + Written::from_size(written)?, + LicenseExchangeState::PlatformChallenge { encryption_data }, + ) + } + Err(err) => { + return Err(custom_err!("ClientNewLicenseRequest", err)); + } + } + } else { + let hwid = self.hardware_id; + match server_license::ClientNewLicenseRequest::from_server_license_request( + &license_request, + &client_random, + &premaster_secret, + &self.username, + &format!("{:X}-{:X}-{:X}-{:X}", hwid[0], hwid[1], hwid[2], hwid[3]), + ) { + Ok((new_license_request, encryption_data)) => { + trace!(?encryption_data, "Successfully generated Client New License Request"); + trace!(message = ?new_license_request, "Send"); + + let written = encode_send_data_request::( + send_data_indication_ctx.initiator_id, + send_data_indication_ctx.channel_id, + &new_license_request.into(), + output, + )?; + + ( + Written::from_size(written)?, + LicenseExchangeState::PlatformChallenge { encryption_data }, + ) + } + Err(error) => { + if let ServerLicenseError::InvalidX509Certificate { + source: error, + cert_der, + } = &error + { + struct BytesHexFormatter<'a>(&'a [u8]); + + impl fmt::Display for BytesHexFormatter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x")?; + self.0.iter().try_for_each(|byte| write!(f, "{byte:02X}")) + } + } + + error!( + %error, + cert_der = %BytesHexFormatter(cert_der), + "Unsupported or invalid X509 certificate received during license exchange step" + ); + } + + return Err(custom_err!("ClientNewLicenseRequest", error)); + } + } + } + } + LicensePdu::LicensingErrorMessage(error_message) => { + if error_message.error_code != server_license::LicenseErrorCode::StatusValidClient { + return Err(custom_err!( + "LicensingErrorMessage", + ServerLicenseError::from(error_message) + )); + } + info!("Server did not initiate license exchange"); + (Written::Nothing, LicenseExchangeState::LicenseExchanged) + } + _ => { + return Err(general_err!( + "unexpected PDU received during LicenseExchangeState::NewLicenseRequest" + )); + } + } + } + + LicenseExchangeState::PlatformChallenge { encryption_data } => { + let send_data_indication_ctx = legacy::decode_send_data_indication(input)?; + + let license_pdu = send_data_indication_ctx + .decode_user_data::() + .with_context("decode during LicenseExchangeState::PlatformChallenge")?; + + match license_pdu { + LicensePdu::ServerPlatformChallenge(challenge) => { + debug!(message = ?challenge, "Received"); + + let challenge_response = + server_license::ClientPlatformChallengeResponse::from_server_platform_challenge( + &challenge, + self.hardware_id, + &encryption_data, + ) + .map_err(|e| custom_err!("ClientPlatformChallengeResponse", e))?; + + debug!(message = ?challenge_response, "Send"); + + let written = encode_send_data_request::( + send_data_indication_ctx.initiator_id, + send_data_indication_ctx.channel_id, + &challenge_response.into(), + output, + )?; + + ( + Written::from_size(written)?, + LicenseExchangeState::UpgradeLicense { encryption_data }, + ) + } + LicensePdu::LicensingErrorMessage(error_message) => { + if error_message.error_code != server_license::LicenseErrorCode::StatusValidClient { + return Err(custom_err!( + "LicensingErrorMessage", + ServerLicenseError::from(error_message) + )); + } + debug!(message = ?error_message, "Received"); + info!("Client licensing completed"); + (Written::Nothing, LicenseExchangeState::LicenseExchanged) + } + _ => { + return Err(general_err!( + "unexpected PDU received during LicenseExchangeState::PlatformChallenge" + )); + } + } + } + + LicenseExchangeState::UpgradeLicense { encryption_data } => { + let send_data_indication_ctx = legacy::decode_send_data_indication(input)?; + + let license_pdu = send_data_indication_ctx + .decode_user_data::() + .with_context("decode during SERVER_NEW_LICENSE/LicenseExchangeState::UpgradeLicense")?; + + match license_pdu { + LicensePdu::ServerUpgradeLicense(upgrade_license) => { + debug!(message = ?upgrade_license, "Received"); + + upgrade_license + .verify_server_license(&encryption_data) + .map_err(|e| custom_err!("license verification", e))?; + + debug!("License verified with success"); + + let license_info = upgrade_license + .new_license_info(&encryption_data) + .map_err(ConnectorError::decode)?; + + self.license_cache.store_license(license_info)? + } + LicensePdu::LicensingErrorMessage(error_message) => { + if error_message.error_code != server_license::LicenseErrorCode::StatusValidClient { + return Err(custom_err!( + "LicensingErrorMessage", + ServerLicenseError::from(error_message) + )); + } + + debug!(message = ?error_message, "Received"); + info!("Client licensing completed"); + } + _ => { + return Err(general_err!( + "unexpected PDU received during LicenseExchangeState::UpgradeLicense" + )); + } + } + + (Written::Nothing, LicenseExchangeState::LicenseExchanged) + } + + LicenseExchangeState::LicenseExchanged => return Err(general_err!("license already exchanged")), + }; + + self.state = next_state; + + Ok(written) + } +} diff --git a/crates/ironrdp-connector/src/macros.rs b/crates/ironrdp-connector/src/macros.rs new file mode 100644 index 00000000..435cc3a1 --- /dev/null +++ b/crates/ironrdp-connector/src/macros.rs @@ -0,0 +1,38 @@ +/// Creates a `ConnectorError` with `General` kind +/// +/// Shorthand for +/// ```rust +/// ::general(context) +/// ``` +#[macro_export] +macro_rules! general_err { + ( $context:expr $(,)? ) => {{ + <$crate::ConnectorError as $crate::ConnectorErrorExt>::general($context) + }}; +} + +/// Creates a `ConnectorError` with `Reason` kind +/// +/// Shorthand for +/// ```rust +/// ::reason(context, reason) +/// ``` +#[macro_export] +macro_rules! reason_err { + ( $context:expr, $($arg:tt)* ) => {{ + <$crate::ConnectorError as $crate::ConnectorErrorExt>::reason($context, format!($($arg)*)) + }}; +} + +/// Creates a `ConnectorError` with `Custom` kind and a source error attached to it +/// +/// Shorthand for +/// ```rust +/// ::custom(context, source) +/// ``` +#[macro_export] +macro_rules! custom_err { + ( $context:expr, $source:expr $(,)? ) => {{ + <$crate::ConnectorError as $crate::ConnectorErrorExt>::custom($context, $source) + }}; +} diff --git a/crates/ironrdp-connector/src/server_name.rs b/crates/ironrdp-connector/src/server_name.rs new file mode 100644 index 00000000..f864db8b --- /dev/null +++ b/crates/ironrdp-connector/src/server_name.rs @@ -0,0 +1,52 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerName(String); + +impl ServerName { + pub fn new(name: impl Into) -> Self { + Self(sanitize_server_name(name.into())) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn into_inner(self) -> String { + self.0 + } +} + +impl From for ServerName { + fn from(value: String) -> Self { + Self::new(value) + } +} + +impl From<&String> for ServerName { + fn from(value: &String) -> Self { + Self::new(value) + } +} + +impl From<&str> for ServerName { + fn from(value: &str) -> Self { + Self::new(value) + } +} + +fn sanitize_server_name(name: String) -> String { + if let Some(addr_split) = name.rsplit_once(':') { + if let Ok(sock_addr) = name.parse::() { + // A socket address, including a port + sock_addr.ip().to_string() + } else if name.parse::().is_ok() { + // An IPv6 address with no port, do not include a port, already sane + name + } else { + // An IPv4 address or server hostname including a port after the `:` token + addr_split.0.to_owned() + } + } else { + // An IPv4 address or server hostname which does not include a port, already sane + name + } +} diff --git a/crates/ironrdp-core/CHANGELOG.md b/crates/ironrdp-core/CHANGELOG.md new file mode 100644 index 00000000..96f7bce7 --- /dev/null +++ b/crates/ironrdp-core/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.1.5](https://github.com/Devolutions/IronRDP/compare/ironrdp-core-v0.1.4...ironrdp-core-v0.1.5)] - 2025-05-28 + +### Features + +- Adds `write_padding` and `read_padding` functions/macros extracted from `ironrdp-pdu` crate + +## [[0.1.4](https://github.com/Devolutions/IronRDP/compare/ironrdp-core-v0.1.3...ironrdp-core-v0.1.4)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-core-v0.1.2...ironrdp-core-v0.1.3)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-core-v0.1.1...ironrdp-core-v0.1.2)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-core/Cargo.toml b/crates/ironrdp-core/Cargo.toml new file mode 100644 index 00000000..554c06a4 --- /dev/null +++ b/crates/ironrdp-core/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ironrdp-core" +version = "0.1.5" +readme = "README.md" +description = "IronRDP common traits and types" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +std = ["alloc", "ironrdp-error/std"] +alloc = ["ironrdp-error/alloc"] + +[dependencies] +ironrdp-error = { path = "../ironrdp-error", version = "0.1" } # public diff --git a/crates/ironrdp-core/LICENSE-APACHE b/crates/ironrdp-core/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-core/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-core/LICENSE-MIT b/crates/ironrdp-core/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-core/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-core/README.md b/crates/ironrdp-core/README.md new file mode 100644 index 00000000..e02bb22d --- /dev/null +++ b/crates/ironrdp-core/README.md @@ -0,0 +1,3 @@ +# IronRDP Core + +IronRDP common traits and types. diff --git a/crates/ironrdp-core/src/as_any.rs b/crates/ironrdp-core/src/as_any.rs new file mode 100644 index 00000000..484c9571 --- /dev/null +++ b/crates/ironrdp-core/src/as_any.rs @@ -0,0 +1,28 @@ +use core::any::Any; + +/// Implement [`AsAny`] for the given type. +#[macro_export] +macro_rules! impl_as_any { + ($t:ty) => { + impl $crate::AsAny for $t { + #[inline] + fn as_any(&self) -> &dyn core::any::Any { + self + } + + #[inline] + fn as_any_mut(&mut self) -> &mut dyn core::any::Any { + self + } + } + }; +} + +/// Type information (`TypeId`) may be retrieved at runtime for this type. +pub trait AsAny: 'static { + /// Returns a reference to the type information for this type. + fn as_any(&self) -> &dyn Any; + + /// Returns a mutable reference to the type information for this type. + fn as_any_mut(&mut self) -> &mut dyn Any; +} diff --git a/crates/ironrdp-core/src/cursor.rs b/crates/ironrdp-core/src/cursor.rs new file mode 100644 index 00000000..16709847 --- /dev/null +++ b/crates/ironrdp-core/src/cursor.rs @@ -0,0 +1,752 @@ +use core::fmt; + +/// Error indicating that there are not enough bytes in the buffer to perform an operation. +#[derive(Copy, Eq, PartialEq, Clone, Debug)] +pub struct NotEnoughBytesError { + received: usize, + expected: usize, +} + +impl NotEnoughBytesError { + /// The number of bytes received. + #[must_use] + #[inline] + pub const fn received(&self) -> usize { + self.received + } + + /// The number of bytes expected. + #[must_use] + #[inline] + pub const fn expected(&self) -> usize { + self.expected + } +} + +impl fmt::Display for NotEnoughBytesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "not enough bytes for operation: received {} bytes, expected {} bytes", + self.received, self.expected + ) + } +} + +#[cfg(feature = "std")] +impl core::error::Error for NotEnoughBytesError {} + +macro_rules! ensure_enough_bytes { + (in: $buf:ident, size: $expected:expr) => {{ + let received = $buf.len(); + let expected = $expected; + if !(received >= expected) { + return Err(NotEnoughBytesError { received, expected }); + } + }}; +} + +/// A cursor for reading bytes from a buffer. +#[derive(Clone, Debug)] +pub struct ReadCursor<'a> { + inner: &'a [u8], + pos: usize, +} + +impl<'a> ReadCursor<'a> { + /// Create a new `ReadCursor` from a byte slice. + #[inline] + pub const fn new(bytes: &'a [u8]) -> Self { + Self { inner: bytes, pos: 0 } + } + + /// Returns the number of bytes remaining. + #[inline] + #[track_caller] + pub const fn len(&self) -> usize { + self.inner.len() - self.pos + } + + /// Returns `true` if there are no bytes remaining. + #[inline] + pub const fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns `true` if there are no bytes remaining. + #[inline] + pub const fn eof(&self) -> bool { + self.is_empty() + } + + /// Returns a slice of the remaining bytes. + #[inline] + #[track_caller] + pub fn remaining(&self) -> &'a [u8] { + let idx = core::cmp::min(self.pos, self.inner.len()); + &self.inner[idx..] + } + + /// Returns two cursors, one with the first `mid` bytes and the other with the remaining bytes. + #[inline] + #[track_caller] + #[must_use] + pub const fn split_at_peek(&self, mid: usize) -> (ReadCursor<'a>, ReadCursor<'a>) { + let (left, right) = self.inner.split_at(self.pos + mid); + let left = ReadCursor { + inner: left, + pos: self.pos, + }; + let right = ReadCursor { inner: right, pos: 0 }; + (left, right) + } + + /// Returns two cursors, one with the first `mid` bytes and the other with the remaining bytes. + /// + /// The current cursor will be moved to the end. + #[inline] + #[track_caller] + #[must_use] + pub fn split_at(&mut self, mid: usize) -> (ReadCursor<'a>, ReadCursor<'a>) { + let res = self.split_at_peek(mid); + self.pos = self.inner.len(); + res + } + + /// Return the inner byte slice. + #[inline] + pub const fn inner(&self) -> &[u8] { + self.inner + } + + /// Returns the current position. + #[inline] + pub const fn pos(&self) -> usize { + self.pos + } + + /// Read an array of `N` bytes. + #[inline] + #[track_caller] + pub fn read_array(&mut self) -> [u8; N] { + let bytes = &self.inner[self.pos..self.pos + N]; + self.pos += N; + bytes.try_into().expect("N-elements array") + } + + /// Read a slice of `n` bytes. + #[inline] + #[track_caller] + pub fn read_slice(&mut self, n: usize) -> &'a [u8] { + let bytes = &self.inner[self.pos..self.pos + n]; + self.pos += n; + bytes + } + + /// Read the remaining bytes. + pub fn read_remaining(&mut self) -> &[u8] { + self.read_slice(self.len()) + } + + /// Read a `u8`. + #[inline] + #[track_caller] + pub fn read_u8(&mut self) -> u8 { + self.read_array::<1>()[0] + } + + /// Try to read a `u8`. + #[inline] + pub fn try_read_u8(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 1); + Ok(self.read_array::<1>()[0]) + } + + /// Read a `i16`. + #[inline] + #[track_caller] + pub fn read_i16(&mut self) -> i16 { + i16::from_le_bytes(self.read_array::<2>()) + } + + /// Read a `i16` in big-endian. + #[inline] + #[track_caller] + pub fn read_i16_be(&mut self) -> i16 { + i16::from_be_bytes(self.read_array::<2>()) + } + + /// Try to read a `i16`. + #[inline] + pub fn try_read_i16(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 2); + Ok(i16::from_le_bytes(self.read_array::<2>())) + } + + /// Try to read a `i16` in big-endian. + #[inline] + pub fn try_read_i16_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 2); + Ok(i16::from_be_bytes(self.read_array::<2>())) + } + + /// Read a `u16`. + #[inline] + #[track_caller] + pub fn read_u16(&mut self) -> u16 { + u16::from_le_bytes(self.read_array::<2>()) + } + + /// Read a `u16` in big-endian. + #[inline] + #[track_caller] + pub fn read_u16_be(&mut self) -> u16 { + u16::from_be_bytes(self.read_array::<2>()) + } + + /// Try to read a `u16`. + #[inline] + pub fn try_read_u16(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 2); + Ok(u16::from_le_bytes(self.read_array::<2>())) + } + + /// Try to read a `u16` in big-endian. + #[inline] + pub fn try_read_u16_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 2); + Ok(u16::from_be_bytes(self.read_array::<2>())) + } + + /// Read a `u32`. + #[inline] + #[track_caller] + pub fn read_u32(&mut self) -> u32 { + u32::from_le_bytes(self.read_array::<4>()) + } + + /// Read a `u32` in big-endian. + #[inline] + #[track_caller] + pub fn read_u32_be(&mut self) -> u32 { + u32::from_be_bytes(self.read_array::<4>()) + } + + /// Try to read a `u32`. + #[inline] + pub fn try_read_u32(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 4); + Ok(u32::from_le_bytes(self.read_array::<4>())) + } + + /// Try to read a `u32` in big-endian. + #[inline] + pub fn try_read_u32_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 4); + Ok(u32::from_be_bytes(self.read_array::<4>())) + } + + /// Read a `u64`. + #[inline] + #[track_caller] + pub fn read_u64(&mut self) -> u64 { + u64::from_le_bytes(self.read_array::<8>()) + } + + /// Read a `u64` in big-endian. + #[inline] + #[track_caller] + pub fn read_u64_be(&mut self) -> u64 { + u64::from_be_bytes(self.read_array::<8>()) + } + + /// Try to read a `u64`. + #[inline] + pub fn try_read_u64(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 8); + Ok(u64::from_le_bytes(self.read_array::<8>())) + } + + /// Try to read a `u64` in big-endian. + #[inline] + pub fn try_read_u64_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 8); + Ok(u64::from_be_bytes(self.read_array::<8>())) + } + + /// Read a `i32`. + #[inline] + pub fn read_i32(&mut self) -> i32 { + i32::from_le_bytes(self.read_array::<4>()) + } + + /// Read a `i32` in big-endian. + #[inline] + pub fn read_i32_be(&mut self) -> i32 { + i32::from_be_bytes(self.read_array::<4>()) + } + + /// Try to read a `i32`. + #[inline] + pub fn try_read_i32(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 4); + Ok(i32::from_le_bytes(self.read_array::<4>())) + } + + /// Try to read a `i32` in big-endian. + #[inline] + pub fn try_read_i32_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 4); + Ok(i32::from_be_bytes(self.read_array::<4>())) + } + + /// Read a `i64`. + #[inline] + pub fn read_i64(&mut self) -> i64 { + i64::from_le_bytes(self.read_array::<8>()) + } + + /// Read a `i64` in big-endian. + #[inline] + pub fn read_i64_be(&mut self) -> i64 { + i64::from_be_bytes(self.read_array::<8>()) + } + + /// Try to read a `i64`. + #[inline] + pub fn try_read_i64(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 8); + Ok(i64::from_le_bytes(self.read_array::<8>())) + } + + /// Try to read a `i64` in big-endian. + #[inline] + pub fn try_read_i64_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 8); + Ok(i64::from_be_bytes(self.read_array::<8>())) + } + + /// Read a `u128`. + #[inline] + pub fn read_u128(&mut self) -> u128 { + u128::from_le_bytes(self.read_array::<16>()) + } + + /// Read a `u128` in big-endian. + #[inline] + pub fn read_u128_be(&mut self) -> u128 { + u128::from_be_bytes(self.read_array::<16>()) + } + + /// Try to read a `u128`. + #[inline] + pub fn try_read_u128(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 16); + Ok(u128::from_le_bytes(self.read_array::<16>())) + } + + /// Try to read a `u128` in big-endian. + #[inline] + pub fn try_read_u128_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 16); + Ok(u128::from_be_bytes(self.read_array::<16>())) + } + + /// Peek at the next `N` bytes without consuming them. + #[inline] + #[track_caller] + pub fn peek(&mut self) -> [u8; N] { + self.inner[self.pos..self.pos + N].try_into().expect("N-elements array") + } + + /// Peek at the next `N` bytes without consuming them. + #[inline] + #[track_caller] + pub fn peek_slice(&mut self, n: usize) -> &'a [u8] { + &self.inner[self.pos..self.pos + n] + } + + /// Peek a `u8` without consuming it. + #[inline] + #[track_caller] + pub fn peek_u8(&mut self) -> u8 { + self.peek::<1>()[0] + } + + /// Try to peek a `u8` without consuming it. + #[inline] + pub fn try_peek_u8(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 1); + Ok(self.peek::<1>()[0]) + } + + /// Peek a `u16` without consuming it. + #[inline] + #[track_caller] + pub fn peek_u16(&mut self) -> u16 { + u16::from_le_bytes(self.peek::<2>()) + } + + /// Peek a big-endian `u16` without consuming it. + #[inline] + #[track_caller] + pub fn peek_u16_be(&mut self) -> u16 { + u16::from_be_bytes(self.peek::<2>()) + } + + /// Try to peek a `u16` without consuming it. + #[inline] + pub fn try_peek_u16(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 2); + Ok(u16::from_le_bytes(self.peek::<2>())) + } + + /// Try to peek a big-endian `u16` without consuming it. + #[inline] + pub fn try_peek_u16_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 2); + Ok(u16::from_be_bytes(self.peek::<2>())) + } + + /// Peek a `u32` without consuming it. + #[inline] + #[track_caller] + pub fn peek_u32(&mut self) -> u32 { + u32::from_le_bytes(self.peek::<4>()) + } + + /// Peek a big-endian `u32` without consuming it. + #[inline] + #[track_caller] + pub fn peek_u32_be(&mut self) -> u32 { + u32::from_be_bytes(self.peek::<4>()) + } + + /// Try to peek a `u32` without consuming it. + #[inline] + pub fn try_peek_u32(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 4); + Ok(u32::from_le_bytes(self.peek::<4>())) + } + + /// Try to peek a big-endian `u32` without consuming it. + #[inline] + pub fn try_peek_u32_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 4); + Ok(u32::from_be_bytes(self.peek::<4>())) + } + + /// Peek a `u64` without consuming it. + #[inline] + #[track_caller] + pub fn peek_u64(&mut self) -> u64 { + u64::from_le_bytes(self.peek::<8>()) + } + + /// Peek a big-endian `u64` without consuming it. + #[inline] + #[track_caller] + pub fn peek_u64_be(&mut self) -> u64 { + u64::from_be_bytes(self.peek::<8>()) + } + + /// Try to peek a `u64` without consuming it. + #[inline] + pub fn try_peek_u64(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 8); + Ok(u64::from_le_bytes(self.peek::<8>())) + } + + /// Try to peek a big-endian `u64` without consuming it. + #[inline] + pub fn try_peek_u64_be(&mut self) -> Result { + ensure_enough_bytes!(in: self, size: 8); + Ok(u64::from_be_bytes(self.peek::<8>())) + } + + /// Advance the cursor by `len` bytes. + #[inline] + #[track_caller] + pub fn advance(&mut self, len: usize) { + self.pos += len; + } + + /// Return a new cursor advanced by `len` bytes. + #[inline] + #[track_caller] + #[must_use] + pub const fn advanced(&'a self, len: usize) -> ReadCursor<'a> { + ReadCursor { + inner: self.inner, + pos: self.pos + len, + } + } + + /// Rewind the cursor by `len` bytes. + #[inline] + #[track_caller] + pub fn rewind(&mut self, len: usize) { + self.pos -= len; + } + + /// Return a new cursor rewinded by `len` bytes. + #[inline] + #[track_caller] + #[must_use] + pub const fn rewinded(&'a self, len: usize) -> ReadCursor<'a> { + ReadCursor { + inner: self.inner, + pos: self.pos - len, + } + } +} + +#[cfg(feature = "std")] +impl std::io::Read for ReadCursor<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let n_to_copy = core::cmp::min(buf.len(), self.len()); + let to_copy = self.read_slice(n_to_copy); + buf.copy_from_slice(to_copy); + Ok(n_to_copy) + } +} + +/// A cursor for writing bytes to a buffer. +#[derive(Debug)] +pub struct WriteCursor<'a> { + inner: &'a mut [u8], + pos: usize, +} + +impl<'a> WriteCursor<'a> { + /// Create a new `WriteCursor` from a mutable slice of bytes. + #[inline] + pub fn new(bytes: &'a mut [u8]) -> Self { + Self { inner: bytes, pos: 0 } + } + + /// Returns the number of bytes remaining. + #[inline] + #[track_caller] + pub const fn len(&self) -> usize { + self.inner.len() - self.pos + } + + /// Returns `true` if there are no bytes remaining. + #[inline] + pub const fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns `true` if there are no bytes remaining. + #[inline] + pub const fn eof(&self) -> bool { + self.is_empty() + } + + /// Returns a slice of the remaining bytes. + #[inline] + #[track_caller] + pub fn remaining(&self) -> &[u8] { + let idx = core::cmp::min(self.pos, self.inner.len()); + &self.inner[idx..] + } + + /// Returns a mutable slice of the remaining bytes. + #[inline] + #[track_caller] + pub fn remaining_mut(&mut self) -> &mut [u8] { + let idx = core::cmp::min(self.pos, self.inner.len()); + &mut self.inner[idx..] + } + + /// Returns the inner byte slice. + #[inline] + pub const fn inner(&self) -> &[u8] { + self.inner + } + + /// Returns the inner mutable byte slice. + #[inline] + pub fn inner_mut(&mut self) -> &mut [u8] { + self.inner + } + + /// Returns the current position of the cursor. + #[inline] + pub const fn pos(&self) -> usize { + self.pos + } + + /// Write an array of bytes to the buffer. + #[inline] + #[track_caller] + pub fn write_array(&mut self, array: [u8; N]) { + self.inner[self.pos..self.pos + N].copy_from_slice(&array); + self.pos += N; + } + + /// Write a slice of bytes to the buffer. + #[inline] + #[track_caller] + pub fn write_slice(&mut self, slice: &[u8]) { + let n = slice.len(); + self.inner[self.pos..self.pos + n].copy_from_slice(slice); + self.pos += n; + } + + /// Write a byte to the buffer. + #[inline] + #[track_caller] + pub fn write_u8(&mut self, value: u8) { + self.write_array(value.to_le_bytes()) + } + + /// Write a signed byte to the buffer. + #[inline] + #[track_caller] + pub fn write_i8(&mut self, value: i8) { + self.write_array(value.to_le_bytes()) + } + + /// Write a little-endian `u16` to the buffer. + #[inline] + #[track_caller] + pub fn write_u16(&mut self, value: u16) { + self.write_array(value.to_le_bytes()) + } + + /// Write a big-endian `u16` to the buffer. + #[inline] + #[track_caller] + pub fn write_u16_be(&mut self, value: u16) { + self.write_array(value.to_be_bytes()) + } + + /// Write a signed little-endian `i16` to the buffer. + #[inline] + #[track_caller] + pub fn write_i16(&mut self, value: i16) { + self.write_array(value.to_le_bytes()) + } + + /// Write a signed big-endian `i16` to the buffer. + #[inline] + #[track_caller] + pub fn write_i16_be(&mut self, value: i16) { + self.write_array(value.to_be_bytes()) + } + + /// Write a little-endian `u32` to the buffer. + #[inline] + #[track_caller] + pub fn write_u32(&mut self, value: u32) { + self.write_array(value.to_le_bytes()) + } + + /// Write a big-endian `u32` to the buffer. + #[inline] + #[track_caller] + pub fn write_u32_be(&mut self, value: u32) { + self.write_array(value.to_be_bytes()) + } + + /// Write a signed little-endian `i32` to the buffer. + #[inline] + #[track_caller] + pub fn write_i32(&mut self, value: i32) { + self.write_array(value.to_le_bytes()) + } + + /// Write a little-endian `u64` to the buffer. + #[inline] + #[track_caller] + pub fn write_u64(&mut self, value: u64) { + self.write_array(value.to_le_bytes()) + } + + /// Write a big-endian `u64` to the buffer. + #[inline] + #[track_caller] + pub fn write_u64_be(&mut self, value: u64) { + self.write_array(value.to_be_bytes()) + } + + /// Write a signed little-endian `i64` to the buffer. + #[inline] + #[track_caller] + pub fn write_i64(&mut self, value: i64) { + self.write_array(value.to_le_bytes()) + } + + /// Write a signed big-endian `i64` to the buffer. + #[inline] + #[track_caller] + pub fn write_i64_be(&mut self, value: i64) { + self.write_array(value.to_be_bytes()) + } + + /// Write a little-endian `u128` to the buffer. + #[inline] + #[track_caller] + pub fn write_u128(&mut self, value: u128) { + self.write_array(value.to_le_bytes()) + } + + /// Write a big-endian `u128` to the buffer. + #[inline] + #[track_caller] + pub fn write_u128_be(&mut self, value: u128) { + self.write_array(value.to_be_bytes()) + } + + /// Advance the cursor by `len` bytes. + #[inline] + #[track_caller] + pub fn advance(&mut self, len: usize) { + self.pos += len; + } + + /// Returns a new cursor advanced by `len` bytes. + #[inline] + #[track_caller] + #[must_use] + pub fn advanced(&'a mut self, len: usize) -> WriteCursor<'a> { + WriteCursor { + inner: self.inner, + pos: self.pos + len, + } + } + + /// Rewind the cursor by `len` bytes. + #[inline] + #[track_caller] + pub fn rewind(&mut self, len: usize) { + self.pos -= len; + } + + /// Returns a new cursor rewinded by `len` bytes. + #[inline] + #[track_caller] + #[must_use] + pub fn rewinded(&'a mut self, len: usize) -> WriteCursor<'a> { + WriteCursor { + inner: self.inner, + pos: self.pos - len, + } + } +} + +#[cfg(feature = "std")] +impl std::io::Write for WriteCursor<'_> { + #[inline] + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.write_slice(buf); + Ok(buf.len()) + } + + #[inline] + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} diff --git a/crates/ironrdp-core/src/decode.rs b/crates/ironrdp-core/src/decode.rs new file mode 100644 index 00000000..910c95ff --- /dev/null +++ b/crates/ironrdp-core/src/decode.rs @@ -0,0 +1,247 @@ +#[cfg(feature = "alloc")] +use alloc::string::String; +use core::fmt; + +use crate::{ + InvalidFieldErr, NotEnoughBytesErr, OtherErr, ReadCursor, UnexpectedMessageTypeErr, UnsupportedValueErr, + UnsupportedVersionErr, +}; + +/// A result type for decoding operations, which can either succeed with a value of type `T` +/// or fail with an [`DecodeError`]. +pub type DecodeResult = Result; + +/// An error type specifically for encoding operations, wrapping an [`DecodeErrorKind`]. +pub type DecodeError = ironrdp_error::Error; + +/// Enum representing different kinds of decode errors. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum DecodeErrorKind { + /// Error when there are not enough bytes to decode. + NotEnoughBytes { + /// Number of bytes received. + received: usize, + /// Number of bytes expected. + expected: usize, + }, + /// Error when a field is invalid. + InvalidField { + /// Name of the invalid field. + field: &'static str, + /// Reason for invalidity. + reason: &'static str, + }, + /// Error when an unexpected message type is encountered. + UnexpectedMessageType { + /// The unexpected message type received. + got: u8, + }, + /// Error when an unsupported version is encountered. + UnsupportedVersion { + /// The unsupported version received. + got: u8, + }, + /// Error when an unsupported value is encountered (with allocation feature). + #[cfg(feature = "alloc")] + UnsupportedValue { + /// Name of the unsupported value. + name: &'static str, + /// The unsupported value. + value: String, + }, + /// Error when an unsupported value is encountered (without allocation feature). + #[cfg(not(feature = "alloc"))] + UnsupportedValue { + /// Name of the unsupported value. + name: &'static str, + }, + /// Generic error for other cases. + Other { + /// Description of the error. + description: &'static str, + }, +} + +#[cfg(feature = "std")] +impl core::error::Error for DecodeErrorKind {} + +impl fmt::Display for DecodeErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotEnoughBytes { received, expected } => write!( + f, + "not enough bytes provided to decode: received {received} bytes, expected {expected} bytes" + ), + Self::InvalidField { field, reason } => { + write!(f, "invalid `{field}`: {reason}") + } + Self::UnexpectedMessageType { got } => { + write!(f, "invalid message type ({got})") + } + Self::UnsupportedVersion { got } => { + write!(f, "unsupported version ({got})") + } + #[cfg(feature = "alloc")] + Self::UnsupportedValue { name, value } => { + write!(f, "unsupported {name} ({value})") + } + #[cfg(not(feature = "alloc"))] + Self::UnsupportedValue { name } => { + write!(f, "unsupported {name}") + } + Self::Other { description } => { + write!(f, "other ({description})") + } + } + } +} + +impl NotEnoughBytesErr for DecodeError { + fn not_enough_bytes(context: &'static str, received: usize, expected: usize) -> Self { + Self::new(context, DecodeErrorKind::NotEnoughBytes { received, expected }) + } +} + +impl InvalidFieldErr for DecodeError { + fn invalid_field(context: &'static str, field: &'static str, reason: &'static str) -> Self { + Self::new(context, DecodeErrorKind::InvalidField { field, reason }) + } +} + +impl UnexpectedMessageTypeErr for DecodeError { + fn unexpected_message_type(context: &'static str, got: u8) -> Self { + Self::new(context, DecodeErrorKind::UnexpectedMessageType { got }) + } +} + +impl UnsupportedVersionErr for DecodeError { + fn unsupported_version(context: &'static str, got: u8) -> Self { + Self::new(context, DecodeErrorKind::UnsupportedVersion { got }) + } +} + +impl UnsupportedValueErr for DecodeError { + #[cfg(feature = "alloc")] + fn unsupported_value(context: &'static str, name: &'static str, value: String) -> Self { + Self::new(context, DecodeErrorKind::UnsupportedValue { name, value }) + } + #[cfg(not(feature = "alloc"))] + fn unsupported_value(context: &'static str, name: &'static str) -> Self { + Self::new(context, DecodeErrorKind::UnsupportedValue { name }) + } +} + +impl OtherErr for DecodeError { + fn other(context: &'static str, description: &'static str) -> Self { + Self::new(context, DecodeErrorKind::Other { description }) + } +} + +/// Trait for types that can be decoded from a byte stream. +/// +/// This trait is implemented by types that can be deserialized from a sequence of bytes. +pub trait Decode<'de>: Sized { + /// Decodes an instance of `Self` from the given byte stream. + /// + /// # Arguments + /// + /// * `src` - A mutable reference to a `ReadCursor` containing the bytes to decode. + /// + /// # Returns + /// + /// Returns a `DecodeResult`, which is either the successfully decoded instance + /// or a `DecodeError` if decoding fails. + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult; +} + +/// Decodes a value of type `T` from a byte slice. +/// +/// This function creates a `ReadCursor` from the input byte slice and uses it to decode +/// a value of type `T` that implements the `Decode` trait. +/// +/// # Arguments +/// +/// * `src` - A byte slice containing the data to be decoded. +/// +/// # Returns +/// +/// Returns a `DecodeResult`, which is either the successfully decoded value +/// or a `DecodeError` if decoding fails. +pub fn decode<'de, T>(src: &'de [u8]) -> DecodeResult +where + T: Decode<'de>, +{ + let mut cursor = ReadCursor::new(src); + T::decode(&mut cursor) +} + +/// Decodes a value of type `T` from a `ReadCursor`. +/// +/// This function uses the provided `ReadCursor` to decode a value of type `T` +/// that implements the `Decode` trait. +/// +/// # Arguments +/// +/// * `src` - A mutable reference to a `ReadCursor` containing the bytes to be decoded. +/// +/// # Returns +/// +/// Returns a `DecodeResult`, which is either the successfully decoded value +/// or a `DecodeError` if decoding fails. +pub fn decode_cursor<'de, T>(src: &mut ReadCursor<'de>) -> DecodeResult +where + T: Decode<'de>, +{ + T::decode(src) +} + +/// Similar to `Decode` but unconditionally returns an owned type. +pub trait DecodeOwned: Sized { + /// Decodes an instance of `Self` from the given byte stream. + /// + /// # Arguments + /// + /// * `src` - A mutable reference to a `ReadCursor` containing the bytes to decode. + /// + /// # Returns + /// + /// Returns a `DecodeResult`, which is either the successfully decoded instance + /// or a `DecodeError` if decoding fails. + fn decode_owned(src: &mut ReadCursor<'_>) -> DecodeResult; +} + +/// Decodes an owned value of type `T` from a byte slice. +/// +/// This function creates a `ReadCursor` from the input byte slice and uses it to decode +/// an owned value of type `T` that implements the `DecodeOwned` trait. +/// +/// # Arguments +/// +/// * `src` - A byte slice containing the data to be decoded. +/// +/// # Returns +/// +/// Returns a `DecodeResult`, which is either the successfully decoded owned value +/// or a `DecodeError` if decoding fails. +pub fn decode_owned(src: &[u8]) -> DecodeResult { + let mut cursor = ReadCursor::new(src); + T::decode_owned(&mut cursor) +} + +/// Decodes an owned value of type `T` from a `ReadCursor`. +/// +/// This function uses the provided `ReadCursor` to decode an owned value of type `T` +/// that implements the `DecodeOwned` trait. +/// +/// # Arguments +/// +/// * `src` - A mutable reference to a `ReadCursor` containing the bytes to be decoded. +/// +/// # Returns +/// +/// Returns a `DecodeResult`, which is either the successfully decoded owned value +/// or a `DecodeError` if decoding fails. +pub fn decode_owned_cursor(src: &mut ReadCursor<'_>) -> DecodeResult { + T::decode_owned(src) +} diff --git a/crates/ironrdp-core/src/encode.rs b/crates/ironrdp-core/src/encode.rs new file mode 100644 index 00000000..a649d66f --- /dev/null +++ b/crates/ironrdp-core/src/encode.rs @@ -0,0 +1,243 @@ +#[cfg(feature = "alloc")] +use alloc::string::String; +#[cfg(feature = "alloc")] +use alloc::{vec, vec::Vec}; +use core::fmt; + +#[cfg(feature = "alloc")] +use crate::WriteBuf; +use crate::{ + InvalidFieldErr, NotEnoughBytesErr, OtherErr, UnexpectedMessageTypeErr, UnsupportedValueErr, UnsupportedVersionErr, + WriteCursor, +}; + +/// A result type for encoding operations, which can either succeed with a value of type `T` +/// or fail with an [`EncodeError`]. +pub type EncodeResult = Result; + +/// An error type specifically for encoding operations, wrapping an [`EncodeErrorKind`]. +pub type EncodeError = ironrdp_error::Error; + +/// Represents the different kinds of errors that can occur during encoding operations. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum EncodeErrorKind { + /// Indicates that there were not enough bytes to complete the encoding operation. + NotEnoughBytes { + /// The number of bytes actually received. + received: usize, + /// The number of bytes expected or required. + expected: usize, + }, + /// Indicates that a field in the data being encoded is invalid. + InvalidField { + /// The name of the invalid field. + field: &'static str, + /// The reason why the field is considered invalid. + reason: &'static str, + }, + /// Indicates that an unexpected message type was encountered during encoding. + UnexpectedMessageType { + /// The unexpected message type that was received. + got: u8, + }, + /// Indicates that an unsupported version was encountered during encoding. + UnsupportedVersion { + /// The unsupported version that was received. + got: u8, + }, + /// Indicates that an unsupported value was encountered during encoding. + #[cfg(feature = "alloc")] + UnsupportedValue { + /// The name of the field or parameter with the unsupported value. + name: &'static str, + /// The unsupported value that was received. + value: String, + }, + /// Indicates that an unsupported value was encountered during encoding (no-alloc version). + #[cfg(not(feature = "alloc"))] + UnsupportedValue { + /// The name of the field or parameter with the unsupported value. + name: &'static str, + }, + /// Represents any other error that doesn't fit into the above categories. + Other { + /// A description of the error. + description: &'static str, + }, +} + +#[cfg(feature = "std")] +impl core::error::Error for EncodeErrorKind {} + +impl fmt::Display for EncodeErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotEnoughBytes { received, expected } => write!( + f, + "not enough bytes provided to decode: received {received} bytes, expected {expected} bytes" + ), + Self::InvalidField { field, reason } => { + write!(f, "invalid `{field}`: {reason}") + } + Self::UnexpectedMessageType { got } => { + write!(f, "invalid message type ({got})") + } + Self::UnsupportedVersion { got } => { + write!(f, "unsupported version ({got})") + } + #[cfg(feature = "alloc")] + Self::UnsupportedValue { name, value } => { + write!(f, "unsupported {name} ({value})") + } + #[cfg(not(feature = "alloc"))] + Self::UnsupportedValue { name } => { + write!(f, "unsupported {name}") + } + Self::Other { description } => { + write!(f, "other ({description})") + } + } + } +} + +impl NotEnoughBytesErr for EncodeError { + fn not_enough_bytes(context: &'static str, received: usize, expected: usize) -> Self { + Self::new(context, EncodeErrorKind::NotEnoughBytes { received, expected }) + } +} + +impl InvalidFieldErr for EncodeError { + fn invalid_field(context: &'static str, field: &'static str, reason: &'static str) -> Self { + Self::new(context, EncodeErrorKind::InvalidField { field, reason }) + } +} + +impl UnexpectedMessageTypeErr for EncodeError { + fn unexpected_message_type(context: &'static str, got: u8) -> Self { + Self::new(context, EncodeErrorKind::UnexpectedMessageType { got }) + } +} + +impl UnsupportedVersionErr for EncodeError { + fn unsupported_version(context: &'static str, got: u8) -> Self { + Self::new(context, EncodeErrorKind::UnsupportedVersion { got }) + } +} + +impl UnsupportedValueErr for EncodeError { + #[cfg(feature = "alloc")] + fn unsupported_value(context: &'static str, name: &'static str, value: String) -> Self { + Self::new(context, EncodeErrorKind::UnsupportedValue { name, value }) + } + #[cfg(not(feature = "alloc"))] + fn unsupported_value(context: &'static str, name: &'static str) -> Self { + Self::new(context, EncodeErrorKind::UnsupportedValue { name }) + } +} + +impl OtherErr for EncodeError { + fn other(context: &'static str, description: &'static str) -> Self { + Self::new(context, EncodeErrorKind::Other { description }) + } +} + +/// PDU that can be encoded into its binary form. +/// +/// The resulting binary payload is a fully encoded PDU that may be sent to the peer. +/// +/// This trait is object-safe and may be used in a dynamic context. +pub trait Encode { + /// Encodes this PDU in-place using the provided `WriteCursor`. + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()>; + + /// Returns the associated PDU name associated. + fn name(&self) -> &'static str; + + /// Computes the size in bytes for this PDU. + fn size(&self) -> usize; +} + +crate::assert_obj_safe!(Encode); + +/// Encodes the given PDU in-place into the provided buffer and returns the number of bytes written. +pub fn encode(pdu: &T, dst: &mut [u8]) -> EncodeResult +where + T: Encode + ?Sized, +{ + let mut cursor = WriteCursor::new(dst); + encode_cursor(pdu, &mut cursor)?; + Ok(cursor.pos()) +} + +/// Encodes the given PDU in-place using the provided `WriteCursor`. +pub fn encode_cursor(pdu: &T, dst: &mut WriteCursor<'_>) -> EncodeResult<()> +where + T: Encode + ?Sized, +{ + pdu.encode(dst) +} + +/// Same as `encode` but resizes the buffer when it is too small to fit the PDU. +#[cfg(feature = "alloc")] +pub fn encode_buf(pdu: &T, buf: &mut WriteBuf) -> EncodeResult +where + T: Encode + ?Sized, +{ + let pdu_size = pdu.size(); + let dst = buf.unfilled_to(pdu_size); + let written = encode(pdu, dst)?; + debug_assert_eq!(written, pdu_size); + buf.advance(written); + Ok(written) +} + +/// Same as `encode` but allocates and returns a new buffer each time. +/// +/// This is a convenience function, but it’s not very resource efficient. +#[cfg(any(feature = "alloc", test))] +pub fn encode_vec(pdu: &T) -> EncodeResult> +where + T: Encode + ?Sized, +{ + let pdu_size = pdu.size(); + let mut buf = vec![0; pdu_size]; + let written = encode(pdu, buf.as_mut_slice())?; + debug_assert_eq!(written, pdu_size); + Ok(buf) +} + +/// Gets the name of this PDU. +pub fn name(pdu: &T) -> &'static str { + pdu.name() +} + +/// Computes the size in bytes for this PDU. +pub fn size(pdu: &T) -> usize { + pdu.size() +} + +#[cfg(feature = "alloc")] +mod legacy { + use super::{Encode, EncodeResult}; + use crate::{ensure_size, WriteCursor}; + + impl Encode for alloc::vec::Vec { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.len()); + + dst.write_slice(self); + Ok(()) + } + + /// Returns the associated PDU name associated. + fn name(&self) -> &'static str { + "legacy-pdu-encode" + } + + /// Computes the size in bytes for this PDU. + fn size(&self) -> usize { + self.len() + } + } +} diff --git a/crates/ironrdp-core/src/error.rs b/crates/ironrdp-core/src/error.rs new file mode 100644 index 00000000..954ad77c --- /dev/null +++ b/crates/ironrdp-core/src/error.rs @@ -0,0 +1,270 @@ +#[cfg(feature = "alloc")] +use alloc::string::String; + +use ironrdp_error::{Error, Source}; + +/// Trait for adding a source to an error type. +pub trait WithSource { + /// Adds a source to the error. + /// + /// # Arguments + /// + /// * `source` - The source error to add. + /// + /// # Returns + /// + /// The error with the added source. + #[must_use] + fn with_source(self, source: E) -> Self; +} + +impl WithSource for Error { + fn with_source(self, source: E) -> Self { + self.with_source(source) + } +} + +/// Trait for creating "not enough bytes" errors. +pub trait NotEnoughBytesErr { + /// Creates a new "not enough bytes" error. + /// + /// # Arguments + /// + /// * `context` - The context in which the error occurred. + /// * `received` - The number of bytes received. + /// * `expected` - The number of bytes expected. + /// + /// # Returns + /// + /// A new error instance. + fn not_enough_bytes(context: &'static str, received: usize, expected: usize) -> Self; +} + +/// Helper function to create a "not enough bytes" error. +/// +/// This function is a convenience wrapper around the `NotEnoughBytesErr` trait. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred. +/// * `received` - The number of bytes received. +/// * `expected` - The number of bytes expected. +/// +/// # Returns +/// +/// A new error instance of type `T` that implements `NotEnoughBytesErr`. +pub fn not_enough_bytes_err(context: &'static str, received: usize, expected: usize) -> T { + T::not_enough_bytes(context, received, expected) +} + +/// Trait for creating "invalid field" errors. +pub trait InvalidFieldErr { + /// Creates a new "invalid field" error. + /// + /// # Arguments + /// + /// * `context` - The context in which the error occurred. + /// * `field` - The name of the invalid field. + /// * `reason` - The reason why the field is invalid. + /// + /// # Returns + /// + /// A new error instance. + fn invalid_field(context: &'static str, field: &'static str, reason: &'static str) -> Self; +} + +/// Helper function to create an "invalid field" error with a source. +/// +/// This function is a convenience wrapper around the `InvalidFieldErr` and `WithSource` traits. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred. +/// * `field` - The name of the invalid field. +/// * `reason` - The reason why the field is invalid. +/// * `source` - The source error to add. +/// +/// # Returns +/// +/// A new error instance of type `T` that implements both `InvalidFieldErr` and `WithSource`. +pub fn invalid_field_err_with_source( + context: &'static str, + field: &'static str, + reason: &'static str, + source: E, +) -> T { + T::invalid_field(context, field, reason).with_source(source) +} + +/// Helper function to create an "invalid field" error. +pub fn invalid_field_err(context: &'static str, field: &'static str, reason: &'static str) -> T { + T::invalid_field(context, field, reason) +} + +/// Trait for creating "unexpected message type" errors. +pub trait UnexpectedMessageTypeErr { + /// Creates a new "unexpected message type" error. + /// + /// # Arguments + /// + /// * `context` - The context in which the error occurred. + /// * `got` - The unexpected message type received. + /// + /// # Returns + /// + /// A new error instance. + fn unexpected_message_type(context: &'static str, got: u8) -> Self; +} + +/// Helper function to create an "unexpected message type" error. +/// +/// This function is a convenience wrapper around the `UnexpectedMessageTypeErr` trait. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred. +/// * `got` - The unexpected message type received. +/// +/// # Returns +/// +/// A new error instance of type `T` that implements `UnexpectedMessageTypeErr`. +pub fn unexpected_message_type_err(context: &'static str, got: u8) -> T { + T::unexpected_message_type(context, got) +} + +/// Trait for creating "unsupported version" errors. +pub trait UnsupportedVersionErr { + /// Creates a new "unsupported version" error. + /// + /// # Arguments + /// + /// * `context` - The context in which the error occurred. + /// * `got` - The unsupported version received. + /// + /// # Returns + /// + /// A new error instance. + fn unsupported_version(context: &'static str, got: u8) -> Self; +} + +/// Helper function to create an "unsupported version" error. +/// +/// This function is a convenience wrapper around the `UnsupportedVersionErr` trait. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred. +/// * `got` - The unsupported version received. +/// +/// # Returns +/// +/// A new error instance of type `T` that implements `UnsupportedVersionErr`. +pub fn unsupported_version_err(context: &'static str, got: u8) -> T { + T::unsupported_version(context, got) +} + +/// Trait for creating "unsupported value" errors. +pub trait UnsupportedValueErr { + /// Creates a new "unsupported value" error when the "alloc" feature is enabled. + /// + /// # Arguments + /// + /// * `context` - The context in which the error occurred. + /// * `name` - The name of the unsupported value. + /// * `value` - The unsupported value. + /// + /// # Returns + /// + /// A new error instance. + #[cfg(feature = "alloc")] + fn unsupported_value(context: &'static str, name: &'static str, value: String) -> Self; + + /// Creates a new "unsupported value" error when the "alloc" feature is disabled. + /// + /// # Arguments + /// + /// * `context` - The context in which the error occurred. + /// * `name` - The name of the unsupported value. + /// + /// # Returns + /// + /// A new error instance. + #[cfg(not(feature = "alloc"))] + fn unsupported_value(context: &'static str, name: &'static str) -> Self; +} + +/// Helper function to create an "unsupported value" error when the "alloc" feature is enabled. +/// +/// This function is a convenience wrapper around the `UnsupportedValueErr` trait. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred. +/// * `name` - The name of the unsupported value. +/// * `value` - The unsupported value. +/// +/// # Returns +/// +/// A new error instance of type `T` that implements `UnsupportedValueErr`.] +#[cfg(feature = "alloc")] +pub fn unsupported_value_err(context: &'static str, name: &'static str, value: String) -> T { + T::unsupported_value(context, name, value) +} + +/// Helper function to create an "unsupported value" error. +#[cfg(not(feature = "alloc"))] +pub fn unsupported_value_err(context: &'static str, name: &'static str) -> T { + T::unsupported_value(context, name) +} + +/// Trait for creating generic "other" errors. +pub trait OtherErr { + /// Creates a new generic "other" error. + /// + /// # Arguments + /// + /// * `context` - The context in which the error occurred. + /// * `description` - A description of the error. + /// + /// # Returns + /// + /// A new error instance. + fn other(context: &'static str, description: &'static str) -> Self; +} + +/// Helper function to create a generic "other" error. +/// +/// This function is a convenience wrapper around the `OtherErr` trait. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred. +/// * `description` - A description of the error. +/// +/// # Returns +/// +/// A new error instance of type `T` that implements `OtherErr`. +pub fn other_err(context: &'static str, description: &'static str) -> T { + T::other(context, description) +} + +/// Helper function to create a generic "other" error with a source. +/// +/// This function is a convenience wrapper around the `OtherErr` and `WithSource` traits. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred. +/// * `description` - A description of the error. +/// * `source` - The source error to add. +/// +/// # Returns +/// +/// A new error instance of type `T` that implements both `OtherErr` and `WithSource`. +pub fn other_err_with_source( + context: &'static str, + description: &'static str, + source: E, +) -> T { + T::other(context, description).with_source(source) +} diff --git a/crates/ironrdp-core/src/into_owned.rs b/crates/ironrdp-core/src/into_owned.rs new file mode 100644 index 00000000..cf451fc3 --- /dev/null +++ b/crates/ironrdp-core/src/into_owned.rs @@ -0,0 +1,8 @@ +/// Used to produce an owned version of a given data. +pub trait IntoOwned: Sized { + /// The resulting type after obtaining ownership. + type Owned: 'static; + + /// Creates owned data from data. + fn into_owned(self) -> Self::Owned; +} diff --git a/crates/ironrdp-core/src/lib.rs b/crates/ironrdp-core/src/lib.rs new file mode 100644 index 00000000..252c2315 --- /dev/null +++ b/crates/ironrdp-core/src/lib.rs @@ -0,0 +1,33 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(clippy::std_instead_of_alloc)] +#![warn(clippy::std_instead_of_core)] +#![cfg_attr(doc, warn(missing_docs))] + +#[cfg(feature = "alloc")] +extern crate alloc; + +mod macros; + +mod as_any; +mod cursor; +mod decode; +mod encode; +mod error; +mod into_owned; +mod padding; +#[cfg(feature = "alloc")] +mod write_buf; + +// Flat API hierarchy of common traits and types + +pub use self::as_any::*; +pub use self::cursor::*; +pub use self::decode::*; +pub use self::encode::*; +pub use self::error::*; +pub use self::into_owned::*; +pub use self::padding::*; +#[cfg(feature = "alloc")] +pub use self::write_buf::*; diff --git a/crates/ironrdp-core/src/macros.rs b/crates/ironrdp-core/src/macros.rs new file mode 100644 index 00000000..52160839 --- /dev/null +++ b/crates/ironrdp-core/src/macros.rs @@ -0,0 +1,426 @@ +/// Asserts that the traits support dynamic dispatch. +/// +/// From +#[macro_export] +macro_rules! assert_obj_safe { + ($($xs:path),+ $(,)?) => { + $(const _: Option<&dyn $xs> = None;)+ + }; +} + +/// Asserts that the type implements _all_ of the given traits. +/// +/// From +#[macro_export] +macro_rules! assert_impl { + ($type:ty: $($trait:path),+ $(,)?) => { + const _: fn() = || { + // Only callable when `$type` implements all traits in `$($trait)+`. + fn assert_impl_all() {} + assert_impl_all::<$type>(); + }; + }; +} + +/// Finds the name of the function in which this macro is expanded +#[macro_export] +macro_rules! function { + // Taken from https://stackoverflow.com/a/40234666 + () => {{ + fn f() {} + fn type_name_of(_: T) -> &'static str { + core::any::type_name::() + } + let name = type_name_of(f); + name.strip_suffix("::f").unwrap() + }}; +} + +/// Creates a "not enough bytes" error with context information. +/// +/// This macro generates an error indicating that there weren't enough bytes +/// in a buffer for a particular operation. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred (optional) +/// * `received` - The number of bytes actually received +/// * `expected` - The number of bytes expected +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::not_enough_bytes_err; +/// +/// let err = not_enough_bytes_err!("parsing header", 5, 10); +/// ``` +/// +/// # Note +/// +/// If the context is not provided, it will use the current function name. +#[macro_export] +macro_rules! not_enough_bytes_err { + ( $context:expr, $received:expr , $expected:expr $(,)? ) => {{ + $crate::not_enough_bytes_err($context, $received, $expected) + }}; + ( $received:expr , $expected:expr $(,)? ) => {{ + $crate::not_enough_bytes_err!($crate::function!(), $received, $expected) + }}; +} + +/// Creates an "invalid field" error with context information. +/// +/// This macro generates an error indicating that a field in a data structure +/// or input is invalid for some reason. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred (optional) +/// * `field` - The name of the invalid field +/// * `reason` - The reason why the field is invalid +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::invalid_field_err; +/// +/// let err = invalid_field_err!("user input", "Age", "must be positive"); +/// ``` +/// +/// # Note +/// +/// If the context is not provided, it will use the current function name. +#[macro_export] +macro_rules! invalid_field_err { + ( $context:expr, $field:expr , $reason:expr $(,)? ) => {{ + $crate::invalid_field_err($context, $field, $reason) + }}; + ( $field:expr , $reason:expr $(,)? ) => {{ + $crate::invalid_field_err!($crate::function!(), $field, $reason) + }}; +} + +/// Creates an "unexpected message type" error with context information. +/// +/// This macro generates an error indicating that an unexpected message type +/// was received in a particular context. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred (optional) +/// * `got` - The unexpected message type that was received +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::unexpected_message_type_err; +/// +/// let err = unexpected_message_type_err!("Erase"); +/// ``` +/// +/// # Note +/// +/// If the context is not provided, it will use the current function name. +#[macro_export] +macro_rules! unexpected_message_type_err { + ( $context:expr, $got:expr $(,)? ) => {{ + $crate::unexpected_message_type_err($context, $got) + }}; + ( $got:expr $(,)? ) => {{ + $crate::unexpected_message_type_err!($crate::function!(), $got) + }}; +} + +/// Creates an "unsupported version" error with context information. +/// +/// This macro generates an error indicating that an unsupported version +/// was encountered in a particular context. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred (optional) +/// * `got` - The unsupported version that was encountered +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::unsupported_version_err; +/// +/// let err = unsupported_version_err!("protocol version", 12); +/// ``` +/// +/// # Note +/// +/// If the context is not provided, it will use the current function name. +#[macro_export] +macro_rules! unsupported_version_err { + ( $context:expr, $got:expr $(,)? ) => {{ + $crate::unsupported_version_err($context, $got) + }}; + ( $got:expr $(,)? ) => {{ + $crate::unsupported_version_err!($crate::function!(), $got) + }}; +} + +/// Creates an "unsupported value" error with context information. +/// +/// This macro generates an error indicating that an unsupported value +/// was encountered for a specific named parameter or field. +/// +/// # Arguments +/// +/// * `context` - The context in which the error occurred (optional) +/// * `name` - The name of the parameter or field with the unsupported value +/// * `value` - The unsupported value that was encountered +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::unsupported_value_err; +/// +/// let err = unsupported_value_err!("configuration", "log_level", "EXTREME"); +/// ``` +/// +/// # Note +/// +/// If the context is not provided, it will use the current function name. +#[macro_export] +macro_rules! unsupported_value_err { + ( $context:expr, $name:expr, $value:expr $(,)? ) => {{ + $crate::unsupported_value_err($context, $name, $value) + }}; + ( $name:expr, $value:expr $(,)? ) => {{ + $crate::unsupported_value_err!($crate::function!(), $name, $value) + }}; +} + +/// Creates a generic "other" error with optional context and source information. +/// +/// This macro generates a generic error that can include a description, context, +/// and an optional source error. It's useful for creating custom errors or +/// wrapping other errors with additional context. +/// +/// # Arguments +/// +/// * `description` - A description of the error (optional) +/// * `context` - The context in which the error occurred (optional) +/// * `source` - The source error, if this error is wrapping another (optional) +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::other_err; +/// +/// // With description and source +/// let source_err = std::io::Error::new(std::io::ErrorKind::Other, "Source error"); +/// let err = other_err!("Something went wrong", source: source_err); +/// +/// // With context and description +/// let err = other_err!("parsing input", "Unexpected end of file"); +/// +/// // With only description +/// let err = other_err!("Operation failed"); +/// +/// // With only source +/// let err = other_err!(source: std::io::Error::new(std::io::ErrorKind::Other, "IO error")); +/// ``` +/// +/// # Note +/// +/// If the context is not provided, it will use the current function name. +#[macro_export] +macro_rules! other_err { + ( $context:expr, source: $source:expr $(,)? ) => {{ + $crate::other_err_with_source($context, "", $source) + }}; + ( $context:expr, $description:expr $(,)? ) => {{ + $crate::other_err($context, $description) + }}; + ( source: $source:expr $(,)? ) => {{ + $crate::other_err!($crate::function!(), source: $source) + }}; + ( $description:expr $(,)? ) => {{ + $crate::other_err!($crate::function!(), $description) + }}; +} + +/// Ensures that a buffer has at least the expected size. +/// +/// This macro checks if the buffer length is greater than or equal to the expected size. +/// If not, it returns a "not enough bytes" error. +/// +/// # Arguments +/// +/// * `ctx` - The context for the error message (optional) +/// * `buf` - The buffer to check +/// * `expected` - The expected minimum size of the buffer +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::ensure_size; +/// +/// fn parse_data(buf: &[u8]) -> Result<(), Error> { +/// ensure_size!(in: buf, size: 10); +/// // ... rest of the parsing logic +/// Ok(()) +/// } +/// ``` +/// +/// # Note +/// +/// If the context is not provided, it will use the current function name. +#[macro_export] +macro_rules! ensure_size { + (ctx: $ctx:expr, in: $buf:ident, size: $expected:expr) => {{ + let received = $buf.len(); + let expected = $expected; + if !(received >= expected) { + return Err($crate::not_enough_bytes_err($ctx, received, expected)); + } + }}; + (in: $buf:ident, size: $expected:expr) => {{ + $crate::ensure_size!(ctx: $crate::function!(), in: $buf, size: $expected) + }}; +} + +/// Ensures that a buffer has at least the fixed part size of a struct. +/// +/// This macro is a specialized version of `ensure_size` that uses the +/// `FIXED_PART_SIZE` constant of the current struct. +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::ensure_fixed_part_size; +/// +/// struct MyStruct { +/// // ... fields +/// } +/// +/// impl MyStruct { +/// const FIXED_PART_SIZE: usize = 20; +/// +/// fn parse(buf: &[u8]) -> Result { +/// ensure_fixed_part_size!(in: buf); +/// // ... parsing logic +/// } +/// } +/// ``` +/// +/// # Note +/// +/// This macro assumes that the current struct has a `FIXED_PART_SIZE` constant defined. +#[macro_export] +macro_rules! ensure_fixed_part_size { + (in: $buf:ident) => {{ + $crate::ensure_size!(ctx: $crate::function!(), in: $buf, size: Self::FIXED_PART_SIZE) + }}; +} + +/// Safely casts a length to a different integer type. +/// +/// This macro attempts to convert a length value to a different integer type, +/// returning an error if the conversion fails due to overflow. +/// +/// # Arguments +/// +/// * `ctx` - The context for the error message (optional) +/// * `field` - The name of the field being cast +/// * `len` - The length value to cast +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::cast_length; +/// +/// fn process_data(data: &[u8]) -> Result<(), Error> { +/// let len: u16 = cast_length!("data length", data.len())?; +/// // ... rest of the processing logic +/// Ok(()) +/// } +/// ``` +/// +/// # Note +/// +/// If the context is not provided, it will use the current function name. +#[macro_export] +macro_rules! cast_length { + ($ctx:expr, $field:expr, $len:expr) => {{ + $len.try_into() + .map_err(|e| $crate::invalid_field_err_with_source($ctx, $field, "too many elements", e)) + }}; + ($field:expr, $len:expr) => {{ + $crate::cast_length!($crate::function!(), $field, $len) + }}; +} + +/// Safely casts an integer to a different integer type. +/// +/// This macro attempts to convert an integer value to a different integer type, +/// returning an error if the conversion fails due to out-of-range issues. +/// +/// # Arguments +/// +/// * `ctx` - The context for the error message (optional) +/// * `field` - The name of the field being cast +/// * `len` - The integer value to cast +/// +/// # Examples +/// +/// ``` +/// use ironrdp_core::cast_int; +/// +/// fn process_value(value: u64) -> Result { +/// let casted_value: i32 = cast_int!("input value", value)?; +/// Ok(casted_value) +/// } +/// ``` +/// +/// # Note +/// +/// If the context is not provided, it will use the current function name. +#[macro_export] +macro_rules! cast_int { + ($ctx:expr, $field:expr, $len:expr) => {{ + $len.try_into().map_err(|e| { + $crate::invalid_field_err_with_source($ctx, $field, "out of range integral type conversion", e) + }) + }}; + ($field:expr, $len:expr) => {{ + $crate::cast_int!($crate::function!(), $field, $len) + }}; +} + +/// Writes zeroes using as few `write_u*` calls as possible. +/// +/// This is similar to `ironrdp_core::padding::write`, but the loop is optimized out when a single +/// operation is enough. +#[macro_export] +macro_rules! write_padding { + ($dst:expr, 1) => { + $dst.write_u8(0) + }; + ($dst:expr, 2) => { + $dst.write_u16(0) + }; + ($dst:expr, 4) => { + $dst.write_u32(0) + }; + ($dst:expr, 8) => { + $dst.write_u64(0) + }; + ($dst:expr, $n:expr) => { + $crate::write_padding($dst, $n) + }; +} + +/// Moves read cursor, ignoring padding bytes. +/// +/// This is similar to `ironrdp_pdu::padding::read`, only exists for consistency with `write_padding!`. +#[macro_export] +macro_rules! read_padding { + ($src:expr, $n:expr) => { + $crate::read_padding($src, $n) + }; +} diff --git a/crates/ironrdp-core/src/padding.rs b/crates/ironrdp-core/src/padding.rs new file mode 100644 index 00000000..1c62168d --- /dev/null +++ b/crates/ironrdp-core/src/padding.rs @@ -0,0 +1,38 @@ +//! Padding handling helpers +//! +//! For maximum compatibility, messages should be generated with padding set to zero, +//! and message recipients should not assume padding has any particular +//! value. + +use crate::{ReadCursor, WriteCursor}; + +/// Writes zeroes using as few `write_u*` calls as possible. +pub fn write_padding(dst: &mut WriteCursor<'_>, mut n: usize) { + loop { + match n { + 0 => break, + 1 => { + dst.write_u8(0); + n -= 1; + } + 2..=3 => { + dst.write_u16(0); + n -= 2; + } + 4..=7 => { + dst.write_u32(0); + n -= 4; + } + _ => { + dst.write_u64(0); + n -= 8; + } + } + } +} + +/// Moves read cursor, ignoring padding bytes. +#[inline] +pub fn read_padding(src: &mut ReadCursor<'_>, n: usize) { + src.advance(n); +} diff --git a/crates/ironrdp-core/src/write_buf.rs b/crates/ironrdp-core/src/write_buf.rs new file mode 100644 index 00000000..09023c00 --- /dev/null +++ b/crates/ironrdp-core/src/write_buf.rs @@ -0,0 +1,237 @@ +use alloc::vec::Vec; +use core::ops::{Index, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive}; + +/// Max capacity to keep for the inner Vec when `WriteBuf::clear` is called. +const MAX_CAPACITY_WHEN_CLEARED: usize = 16384; // 16 kib + +/// Growable buffer backed by a [`Vec`] that is incrementally filled. +/// +/// This type is tracking the filled region and provides methods to +/// grow and write into the unfilled region. +/// +/// Memory layout can be visualized as: +/// +/// ```not_rust +/// [ Vec capacity ] +/// [ filled | unfilled | ] +/// [ initialized | uninitialized ] +/// ``` +pub struct WriteBuf { + inner: Vec, + filled: usize, +} + +impl WriteBuf { + /// Constructs a new, empty `WriteBuf`. + /// + /// The underlying buffer will not allocate until bytes are written to it. + #[inline] + pub const fn new() -> Self { + Self { + inner: Vec::new(), + filled: 0, + } + } + + /// Constructs a new `WriteBuf` from a given `Vec`. + #[inline] + pub const fn from_vec(buffer: Vec) -> Self { + Self { + inner: buffer, + filled: 0, + } + } + + /// Consumes the `WriteBuf`, returning the underlying `Vec`. + #[inline] + pub fn into_inner(self) -> Vec { + self.inner + } + + /// Returns length of the filled region. + /// + /// This is always equal to the starting index for the unfilled initialized portion of the buffer. + #[inline] + pub const fn filled_len(&self) -> usize { + self.filled + } + + /// Returns a shared reference to the filled portion of the buffer. + #[inline] + pub fn filled(&self) -> &[u8] { + &self.inner[..self.filled] + } + + /// Ensures initialized and unfilled portion of the buffer is big enough for `additional` more bytes. + #[inline] + pub fn initialize(&mut self, additional: usize) { + if self.inner.len() < self.filled + additional { + self.inner.resize(self.filled + additional, 0); + } + } + + /// Returns a mutable reference to the first n bytes of the unfilled part of the buffer, + /// allocating additional memory as necessary. + #[inline] + pub fn unfilled_to(&mut self, n: usize) -> &mut [u8] { + self.initialize(n); + &mut self.inner[self.filled..self.filled + n] + } + + /// Returns a mutable reference to the unfilled part of the buffer. + #[inline] + pub fn unfilled_mut(&mut self) -> &mut [u8] { + &mut self.inner[self.filled..] + } + + /// Writes an array of bytes into the buffer. + #[inline] + pub fn write_array(&mut self, array: [u8; N]) { + self.initialize(N); + self.inner[self.filled..self.filled + N].copy_from_slice(&array); + self.filled += N; + } + + /// Writes a slice of bytes into the buffer. + #[inline] + pub fn write_slice(&mut self, slice: &[u8]) { + let n = slice.len(); + self.initialize(n); + self.inner[self.filled..self.filled + n].copy_from_slice(slice); + self.filled += n; + } + + /// Writes a single byte into the buffer. + #[inline] + pub fn write_u8(&mut self, value: u8) { + self.write_array(value.to_le_bytes()) + } + + /// Writes a `u16` into the buffer as little-endian. + #[inline] + pub fn write_u16(&mut self, value: u16) { + self.write_array(value.to_le_bytes()) + } + + /// Writes a `u16` into the buffer as big-endian. + #[inline] + pub fn write_u16_be(&mut self, value: u16) { + self.write_array(value.to_be_bytes()) + } + + /// Writes a `u32` into the buffer as little-endian. + #[inline] + pub fn write_u32(&mut self, value: u32) { + self.write_array(value.to_le_bytes()) + } + + /// Writes a `u32` into the buffer as big-endian. + #[inline] + pub fn write_u32_be(&mut self, value: u32) { + self.write_array(value.to_be_bytes()) + } + + /// Writes a `u64` into the buffer as little-endian. + #[inline] + pub fn write_u64(&mut self, value: u64) { + self.write_array(value.to_le_bytes()) + } + + /// Writes a `u64` into the buffer as big-endian. + #[inline] + pub fn write_u64_be(&mut self, value: u64) { + self.write_array(value.to_be_bytes()) + } + + /// Set the filled cursor to the very beginning of the buffer. + /// + /// If the buffer grew big, it is shrunk in order to reclaim memory. + #[inline] + pub fn clear(&mut self) { + self.filled = 0; + self.inner.shrink_to(MAX_CAPACITY_WHEN_CLEARED); + } + + /// Advances the buffer’s cursor of `len` bytes. + #[inline] + pub fn advance(&mut self, len: usize) { + self.filled += len; + debug_assert!(self.filled <= self.inner.len()); + } +} + +impl Default for WriteBuf { + fn default() -> Self { + Self::new() + } +} + +#[cfg(feature = "std")] +impl std::io::Write for WriteBuf { + #[inline] + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.write_slice(buf); + Ok(buf.len()) + } + + #[inline] + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +// Allows the user to get a slice of the filled region using indexing operations (e.g.: buf[..], buf[..10], buf[2..8]). + +impl Index> for WriteBuf { + type Output = [u8]; + + #[inline] + fn index(&self, range: Range) -> &Self::Output { + &self.filled()[range] + } +} + +impl Index> for WriteBuf { + type Output = [u8]; + + #[inline] + fn index(&self, range: RangeFrom) -> &Self::Output { + &self.filled()[range] + } +} + +impl Index for WriteBuf { + type Output = [u8]; + + #[inline] + fn index(&self, _: RangeFull) -> &Self::Output { + self.filled() + } +} + +impl Index> for WriteBuf { + type Output = [u8]; + + #[inline] + fn index(&self, range: RangeInclusive) -> &Self::Output { + &self.filled()[range] + } +} + +impl Index> for WriteBuf { + type Output = [u8]; + + #[inline] + fn index(&self, range: RangeTo) -> &Self::Output { + &self.filled()[range] + } +} + +impl Index> for WriteBuf { + type Output = [u8]; + + #[inline] + fn index(&self, range: RangeToInclusive) -> &Self::Output { + &self.filled()[range] + } +} diff --git a/crates/ironrdp-displaycontrol/CHANGELOG.md b/crates/ironrdp-displaycontrol/CHANGELOG.md new file mode 100644 index 00000000..9801a966 --- /dev/null +++ b/crates/ironrdp-displaycontrol/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-displaycontrol-v0.1.3...ironrdp-displaycontrol-v0.2.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + + + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-displaycontrol-v0.1.2...ironrdp-displaycontrol-v0.1.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-displaycontrol-v0.1.1...ironrdp-displaycontrol-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-displaycontrol-v0.1.0...ironrdp-displaycontrol-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-displaycontrol/Cargo.toml b/crates/ironrdp-displaycontrol/Cargo.toml new file mode 100644 index 00000000..0c336d50 --- /dev/null +++ b/crates/ironrdp-displaycontrol/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ironrdp-displaycontrol" +version = "0.4.0" +readme = "README.md" +description = "Display control dynamic channel extension implementation" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.4" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +tracing = { version = "0.1", features = ["log"] } + +[lints] +workspace = true diff --git a/crates/ironrdp-displaycontrol/LICENSE-APACHE b/crates/ironrdp-displaycontrol/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-displaycontrol/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-displaycontrol/LICENSE-MIT b/crates/ironrdp-displaycontrol/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-displaycontrol/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-displaycontrol/README.md b/crates/ironrdp-displaycontrol/README.md new file mode 100644 index 00000000..2255c08d --- /dev/null +++ b/crates/ironrdp-displaycontrol/README.md @@ -0,0 +1,9 @@ +# IronRDP Display Control Virtual Channel Extension [MS-RDPEDISP][1] implementation. + +Display Control Virtual Channel Extension [MS-RDPEDISP][1] implementation. + +This library includes: +- Display Control DVC PDUs parsing +- Display Control DVC processing (TODO) + +[1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/d2954508-f487-48bc-8731-39743e0854a9 \ No newline at end of file diff --git a/crates/ironrdp-displaycontrol/src/client.rs b/crates/ironrdp-displaycontrol/src/client.rs new file mode 100644 index 00000000..74764896 --- /dev/null +++ b/crates/ironrdp-displaycontrol/src/client.rs @@ -0,0 +1,88 @@ +use ironrdp_core::{impl_as_any, Decode as _, EncodeResult, ReadCursor}; +use ironrdp_dvc::{encode_dvc_messages, DvcClientProcessor, DvcMessage, DvcProcessor}; +use ironrdp_pdu::{decode_err, PduResult}; +use ironrdp_svc::{ChannelFlags, SvcMessage}; +use tracing::debug; + +use crate::pdu::{DisplayControlCapabilities, DisplayControlMonitorLayout, DisplayControlPdu}; +use crate::CHANNEL_NAME; + +/// A client for the Display Control Virtual Channel. +pub struct DisplayControlClient { + /// A callback that will be called when capabilities are received from the server. + on_capabilities_received: OnCapabilitiesReceived, + /// Indicates whether the capabilities have been received from the server. + ready: bool, +} + +impl DisplayControlClient { + /// Creates a new [`DisplayControlClient`] with the given `callback`. + /// + /// The `callback` will be called when capabilities are received from the server. + /// It is important to note that the channel will not be fully operational until the capabilities are received. + /// Attempting to send messages before the capabilities are received will result in an error or a silent failure. + pub fn new(callback: F) -> Self + where + F: Fn(DisplayControlCapabilities) -> PduResult> + Send + 'static, + { + Self { + on_capabilities_received: Box::new(callback), + ready: false, + } + } + + pub fn ready(&self) -> bool { + self.ready + } + + /// Builds a [`DisplayControlPdu::MonitorLayout`] with a single primary monitor + /// with the given `width` and `height`, and wraps it as an [`SvcMessage`]. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent. + /// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm. + /// + /// Use [`crate::pdu::MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function + /// to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn encode_single_primary_monitor( + &self, + channel_id: u32, + width: u32, + height: u32, + scale_factor: Option, + physical_dims: Option<(u32, u32)>, + ) -> EncodeResult> { + // TODO: prevent resolution with values greater than max monitor area received in caps. + let pdu: DisplayControlPdu = + DisplayControlMonitorLayout::new_single_primary_monitor(width, height, scale_factor, physical_dims)?.into(); + debug!(?pdu, "Sending monitor layout"); + encode_dvc_messages(channel_id, vec![Box::new(pdu)], ChannelFlags::empty()) + } +} + +impl_as_any!(DisplayControlClient); + +impl DvcProcessor for DisplayControlClient { + fn channel_name(&self) -> &str { + CHANNEL_NAME + } + + fn start(&mut self, _channel_id: u32) -> PduResult> { + Ok(Vec::new()) + } + + fn process(&mut self, _channel_id: u32, payload: &[u8]) -> PduResult> { + let caps = DisplayControlCapabilities::decode(&mut ReadCursor::new(payload)).map_err(|e| decode_err!(e))?; + debug!("Received {:?}", caps); + self.ready = true; + (self.on_capabilities_received)(caps) + } +} + +impl DvcClientProcessor for DisplayControlClient {} + +type OnCapabilitiesReceived = Box PduResult> + Send>; diff --git a/crates/ironrdp-displaycontrol/src/lib.rs b/crates/ironrdp-displaycontrol/src/lib.rs new file mode 100644 index 00000000..6ffa23af --- /dev/null +++ b/crates/ironrdp-displaycontrol/src/lib.rs @@ -0,0 +1,8 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +pub const CHANNEL_NAME: &str = "Microsoft::Windows::RDS::DisplayControl"; + +pub mod client; +pub mod pdu; +pub mod server; diff --git a/crates/ironrdp-displaycontrol/src/pdu/mod.rs b/crates/ironrdp-displaycontrol/src/pdu/mod.rs new file mode 100644 index 00000000..d89c632f --- /dev/null +++ b/crates/ironrdp-displaycontrol/src/pdu/mod.rs @@ -0,0 +1,757 @@ +//! Display Update Virtual Channel Extension PDUs [MS-RDPEDISP][1] implementation. +//! +//! [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/d2954508-f487-48bc-8731-39743e0854a9 + +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; +use ironrdp_dvc::DvcEncode; +use tracing::warn; + +const DISPLAYCONTROL_PDU_TYPE_CAPS: u32 = 0x00000005; +const DISPLAYCONTROL_PDU_TYPE_MONITOR_LAYOUT: u32 = 0x00000002; + +const DISPLAYCONTROL_MONITOR_PRIMARY: u32 = 0x00000001; + +// Set out expectations about supported PDU values. 1024 monitors with 8k*8k pixel area is +// already excessive, (this extension only supports displays up to 8k*8k) therefore we could safely +// use those limits to detect ill-formed PDUs and set out invariants. +const MAX_SUPPORTED_MONITORS: u16 = 1024; +const MAX_MONITOR_AREA_FACTOR: u16 = 1024 * 16; + +/// Display Update Virtual Channel message (PDU prefixed with `DISPLAYCONTROL_HEADER`) +/// +/// INVARIANTS: size of encoded inner PDU is always less than `u32::MAX - Self::FIXED_PART_SIZE` +/// (See [`DisplayControlCapabilities`] & [`DisplayControlMonitorLayout`] invariants) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DisplayControlPdu { + Caps(DisplayControlCapabilities), + MonitorLayout(DisplayControlMonitorLayout), +} + +impl DisplayControlPdu { + const NAME: &'static str = "DISPLAYCONTROL_HEADER"; + const FIXED_PART_SIZE: usize = 4 /* Type */ + 4 /* Length */; +} + +impl Encode for DisplayControlPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let (kind, payload_length) = match self { + DisplayControlPdu::Caps(caps) => (DISPLAYCONTROL_PDU_TYPE_CAPS, caps.size()), + DisplayControlPdu::MonitorLayout(layout) => (DISPLAYCONTROL_PDU_TYPE_MONITOR_LAYOUT, layout.size()), + }; + + // This will never overflow as per invariants. + #[expect(clippy::arithmetic_side_effects)] + let pdu_size = cast_length!("pdu size", payload_length + Self::FIXED_PART_SIZE)?; + + // Write `DISPLAYCONTROL_HEADER` fields. + dst.write_u32(kind); + dst.write_u32(pdu_size); + + match self { + DisplayControlPdu::Caps(caps) => caps.encode(dst), + DisplayControlPdu::MonitorLayout(layout) => layout.encode(dst), + }?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + // As per invariants: This will never overflow. + #[expect(clippy::arithmetic_side_effects)] + let size = Self::FIXED_PART_SIZE + + match self { + DisplayControlPdu::Caps(caps) => caps.size(), + DisplayControlPdu::MonitorLayout(layout) => layout.size(), + }; + + size + } +} + +impl DvcEncode for DisplayControlPdu {} + +impl<'de> Decode<'de> for DisplayControlPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + // Read `DISPLAYCONTROL_HEADER` fields. + let kind = src.read_u32(); + let pdu_length = src.read_u32(); + + let _payload_length = pdu_length + .checked_sub(Self::FIXED_PART_SIZE.try_into().expect("always in range")) + .ok_or_else(|| invalid_field_err!("Length", "Display control PDU length is too small"))?; + + match kind { + DISPLAYCONTROL_PDU_TYPE_CAPS => { + let caps = DisplayControlCapabilities::decode(src)?; + Ok(DisplayControlPdu::Caps(caps)) + } + DISPLAYCONTROL_PDU_TYPE_MONITOR_LAYOUT => { + let layout = DisplayControlMonitorLayout::decode(src)?; + Ok(DisplayControlPdu::MonitorLayout(layout)) + } + _ => Err(invalid_field_err!("Type", "Unknown display control PDU type")), + } + } +} + +impl From for DisplayControlPdu { + fn from(caps: DisplayControlCapabilities) -> Self { + Self::Caps(caps) + } +} + +impl From for DisplayControlPdu { + fn from(layout: DisplayControlMonitorLayout) -> Self { + Self::MonitorLayout(layout) + } +} + +/// 2.2.2.1 DISPLAYCONTROL_CAPS_PDU +/// +/// Display control channel capabilities PDU. +/// +/// INVARIANTS: +/// 0 <= max_num_monitors <= MAX_SUPPORTED_MONITORS +/// 0 <= max_monitor_area_factor_a <= MAX_MONITOR_AREA_FACTOR +/// 0 <= max_monitor_area_factor_b <= MAX_MONITOR_AREA_FACTOR +/// +/// [2.2.2.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/8989a211-984e-4ecc-80f3-60694fc4b476 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DisplayControlCapabilities { + max_num_monitors: u32, + max_monitor_area_factor_a: u32, + max_monitor_area_factor_b: u32, + max_monitor_area: u64, +} + +impl DisplayControlCapabilities { + const NAME: &'static str = "DISPLAYCONTROL_CAPS_PDU"; + const FIXED_PART_SIZE: usize = 4 /* MaxNumMonitors */ + + 4 /* MaxMonitorAreaFactorA */ + + 4 /* MaxMonitorAreaFactorB */; + + pub fn new( + max_num_monitors: u32, + max_monitor_area_factor_a: u32, + max_monitor_area_factor_b: u32, + ) -> DecodeResult { + let max_monitor_area = + calculate_monitor_area(max_num_monitors, max_monitor_area_factor_a, max_monitor_area_factor_b)?; + + Ok(Self { + max_num_monitors, + max_monitor_area_factor_a, + max_monitor_area_factor_b, + max_monitor_area, + }) + } + + pub fn max_monitor_area(&self) -> u64 { + self.max_monitor_area + } +} + +impl Encode for DisplayControlCapabilities { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + dst.write_u32(self.max_num_monitors); + dst.write_u32(self.max_monitor_area_factor_a); + dst.write_u32(self.max_monitor_area_factor_b); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for DisplayControlCapabilities { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let max_num_monitors = src.read_u32(); + let max_monitor_area_factor_a = src.read_u32(); + let max_monitor_area_factor_b = src.read_u32(); + + let max_monitor_area = + calculate_monitor_area(max_num_monitors, max_monitor_area_factor_a, max_monitor_area_factor_b)?; + + Ok(Self { + max_num_monitors, + max_monitor_area_factor_a, + max_monitor_area_factor_b, + max_monitor_area, + }) + } +} + +/// [2.2.2.2] DISPLAYCONTROL_MONITOR_LAYOUT_PDU +/// +/// Sent from client to server to notify about new monitor layout (e.g screen resize). +/// +/// INVARIANTS: +/// 0 <= monitors.length() <= MAX_SUPPORTED_MONITORS +/// +/// [2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/22741217-12a0-4fb8-b5a0-df43905aaf06 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DisplayControlMonitorLayout { + monitors: Vec, +} + +impl DisplayControlMonitorLayout { + const NAME: &'static str = "DISPLAYCONTROL_MONITOR_LAYOUT_PDU"; + const FIXED_PART_SIZE: usize = 4 /* MonitorLayoutSize */ + 4 /* NumMonitors */; + + pub fn new(monitors: &[MonitorLayoutEntry]) -> EncodeResult { + if monitors.len() > MAX_SUPPORTED_MONITORS.into() { + return Err(invalid_field_err!("NumMonitors", "Too many monitors",)); + } + + let primary_monitors_count = monitors.iter().filter(|monitor| monitor.is_primary()).count(); + + if primary_monitors_count != 1 { + return Err(invalid_field_err!( + "PrimaryMonitor", + "There must be exactly one primary monitor" + )); + } + + Ok(Self { + monitors: monitors.to_vec(), + }) + } + + /// Creates a new [`DisplayControlMonitorLayout`] with a single primary monitor + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent. + /// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm. + /// + /// Use [`MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function + /// to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn new_single_primary_monitor( + width: u32, + height: u32, + scale_factor: Option, + physical_dims: Option<(u32, u32)>, + ) -> EncodeResult { + let entry = MonitorLayoutEntry::new_primary(width, height)?.with_orientation(if width > height { + MonitorOrientation::Landscape + } else { + MonitorOrientation::Portrait + }); + + let entry = if let Some(scale_factor) = scale_factor { + entry + .with_desktop_scale_factor(scale_factor)? + .with_device_scale_factor(DeviceScaleFactor::Scale100Percent) + } else { + entry + }; + + let entry = if let Some((physical_width, physical_height)) = physical_dims { + entry.with_physical_dimensions(physical_width, physical_height)? + } else { + entry + }; + + DisplayControlMonitorLayout::new(&[entry]) + } + + pub fn monitors(&self) -> &[MonitorLayoutEntry] { + &self.monitors + } +} + +impl Encode for DisplayControlMonitorLayout { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(MonitorLayoutEntry::FIXED_PART_SIZE.try_into().expect("always in range")); + + let monitors_count: u32 = self + .monitors + .len() + .try_into() + .map_err(|_| invalid_field_err!("NumMonitors", "Number of monitors is too big"))?; + + dst.write_u32(monitors_count); + + for monitor in &self.monitors { + monitor.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + // As per invariants: This will never overflow: + // 0 <= Self::FIXED_PART_SIZE + MAX_SUPPORTED_MONITORS * MonitorLayoutEntry::FIXED_PART_SIZE < u16::MAX + #[expect(clippy::arithmetic_side_effects)] + let size = Self::FIXED_PART_SIZE + self.monitors.iter().map(|monitor| monitor.size()).sum::(); + + size + } +} + +impl<'de> Decode<'de> for DisplayControlMonitorLayout { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let monitor_layout_size = src.read_u32(); + + if monitor_layout_size != MonitorLayoutEntry::FIXED_PART_SIZE.try_into().expect("always in range") { + return Err(invalid_field_err!( + "MonitorLayoutSize", + "Monitor layout size is invalid" + )); + } + + let num_monitors = cast_length!("number of monitors", src.read_u32())?; + + if num_monitors > MAX_SUPPORTED_MONITORS.into() { + return Err(invalid_field_err!("NumMonitors", "Too many monitors")); + } + + let mut monitors = Vec::with_capacity(num_monitors); + for _ in 0..num_monitors { + let monitor = MonitorLayoutEntry::decode(src)?; + monitors.push(monitor); + } + + Ok(Self { monitors }) + } +} + +/// [2.2.2.2.1] DISPLAYCONTROL_MONITOR_LAYOUT_PDU +/// +/// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonitorLayoutEntry { + is_primary: bool, + left: i32, + top: i32, + width: u32, + height: u32, + physical_width: u32, + physical_height: u32, + orientation: u32, + desktop_scale_factor: u32, + device_scale_factor: u32, +} + +macro_rules! validate_dimensions { + ($width:expr, $height:expr) => {{ + if !(200..=8192).contains(&$width) { + return Err(invalid_field_err!("Width", "Monitor width is out of range")); + } + if $width % 2 != 0 { + return Err(invalid_field_err!("Width", "Monitor width cannot be odd")); + } + if !(200..=8192).contains(&$height) { + return Err(invalid_field_err!("Height", "Monitor height is out of range")); + } + Ok(()) + }}; +} + +impl MonitorLayoutEntry { + const FIXED_PART_SIZE: usize = 4 /* Flags */ + + 4 /* Left */ + + 4 /* Top */ + + 4 /* Width */ + + 4 /* Height */ + + 4 /* PhysicalWidth */ + + 4 /* PhysicalHeight */ + + 4 /* Orientation */ + + 4 /* DesktopScaleFactor */ + + 4 /* DeviceScaleFactor */; + + const NAME: &'static str = "DISPLAYCONTROL_MONITOR_LAYOUT"; + + /// Creates a new [`MonitorLayoutEntry`]. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + fn new_impl(mut width: u32, height: u32) -> EncodeResult { + if width % 2 != 0 { + let prev_width = width; + width = width.saturating_sub(1); + warn!( + "Monitor width cannot be odd, adjusting from [{}] to [{}]", + prev_width, width + ) + } + + validate_dimensions!(width, height)?; + + Ok(Self { + is_primary: false, + left: 0, + top: 0, + width, + height, + physical_width: 0, + physical_height: 0, + orientation: 0, + desktop_scale_factor: 0, + device_scale_factor: 0, + }) + } + + /// Adjusts the display size to be within the valid range. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// + /// Functions that create [`MonitorLayoutEntry`] should typically use this function to adjust the display size first. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn adjust_display_size(width: u32, height: u32) -> (u32, u32) { + fn constrain(value: u32) -> u32 { + value.clamp(200, 8192) + } + + let mut width = width; + if width % 2 != 0 { + width = width.saturating_sub(1); + } + + (constrain(width), constrain(height)) + } + + /// Creates a new primary [`MonitorLayoutEntry`]. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// + /// Use [`MonitorLayoutEntry::adjust_display_size`] before calling this function to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn new_primary(width: u32, height: u32) -> EncodeResult { + let mut entry = Self::new_impl(width, height)?; + entry.is_primary = true; + Ok(entry) + } + + /// Creates a new primary [`MonitorLayoutEntry`]. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// + /// Use [`MonitorLayoutEntry::adjust_display_size`] before calling this function to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn new_secondary(width: u32, height: u32) -> EncodeResult { + Self::new_impl(width, height) + } + + /// Sets the monitor's orientation. (Default is [`MonitorOrientation::Landscape`]) + #[must_use] + pub fn with_orientation(mut self, orientation: MonitorOrientation) -> Self { + self.orientation = orientation.angle(); + self + } + + /// Sets the monitor's position (left, top) in pixels. (Default is (0, 0)) + /// + /// Note: The primary monitor position must be always (0, 0). + pub fn with_position(mut self, left: i32, top: i32) -> EncodeResult { + validate_position(left, top, self.is_primary)?; + + self.left = left; + self.top = top; + + Ok(self) + } + + /// Sets the monitor's device scale factor in percent. (Default is [`DeviceScaleFactor::Scale100Percent`]) + #[must_use] + pub fn with_device_scale_factor(mut self, device_scale_factor: DeviceScaleFactor) -> Self { + self.device_scale_factor = device_scale_factor.value(); + self + } + + /// Sets the monitor's desktop scale factor in percent. + /// + /// NOTE: As specified in [MS-RDPEDISP], if the desktop scale factor is not in the valid range + /// (100..=500 percent), the monitor desktop scale factor is considered invalid and should be ignored. + pub fn with_desktop_scale_factor(mut self, desktop_scale_factor: u32) -> EncodeResult { + validate_desktop_scale_factor(desktop_scale_factor)?; + + self.desktop_scale_factor = desktop_scale_factor; + Ok(self) + } + + /// Sets the monitor's physical dimensions in millimeters. + /// + /// NOTE: As specified in [MS-RDPEDISP], if the physical dimensions are not in the valid range + /// (10..=10000 millimeters), the monitor physical dimensions are considered invalid and + /// should be ignored. + pub fn with_physical_dimensions(mut self, physical_width: u32, physical_height: u32) -> EncodeResult { + validate_physical_dimensions(physical_width, physical_height)?; + + self.physical_width = physical_width; + self.physical_height = physical_height; + Ok(self) + } + + pub fn is_primary(&self) -> bool { + self.is_primary + } + + /// Returns the monitor's position (left, top) in pixels. + pub fn position(&self) -> Option<(i32, i32)> { + validate_position(self.left, self.top, self.is_primary).ok()?; + + Some((self.left, self.top)) + } + + /// Returns the monitor's dimensions (width, height) in pixels. + pub fn dimensions(&self) -> (u32, u32) { + (self.width, self.height) + } + + /// Returns the monitor's orientation if it is valid. + /// + /// NOTE: As specified in [MS-RDPEDISP], if the orientation is not one of the valid values + /// (0, 90, 180, 270), the monitor orientation is considered invalid and should be ignored. + pub fn orientation(&self) -> Option { + MonitorOrientation::from_angle(self.orientation) + } + + /// Returns the monitor's physical dimensions (width, height) in millimeters. + /// + /// NOTE: As specified in [MS-RDPEDISP], if the physical dimensions are not in the valid range + /// (10..=10000 millimeters), the monitor physical dimensions are considered invalid and + /// should be ignored. + pub fn physical_dimensions(&self) -> Option<(u32, u32)> { + validate_physical_dimensions(self.physical_width, self.physical_height).ok()?; + Some((self.physical_width, self.physical_height)) + } + + /// Returns the monitor's device scale factor in percent if it is valid. + /// + /// NOTE: As specified in [MS-RDPEDISP], if the desktop scale factor is not in the valid range + /// (100..=500 percent), the monitor desktop scale factor is considered invalid and should be ignored. + /// + /// IMPORTANT: When processing scale factors, make sure that both desktop and device scale factors + /// are valid, otherwise they both should be ignored. + pub fn desktop_scale_factor(&self) -> Option { + validate_desktop_scale_factor(self.desktop_scale_factor).ok()?; + + Some(self.desktop_scale_factor) + } + + /// Returns the monitor's device scale factor in percent if it is valid. + /// + /// IMPORTANT: When processing scale factors, make sure that both desktop and device scale factors + /// are valid, otherwise they both should be ignored. + pub fn device_scale_factor(&self) -> Option { + match self.device_scale_factor { + 100 => Some(DeviceScaleFactor::Scale100Percent), + 140 => Some(DeviceScaleFactor::Scale140Percent), + 180 => Some(DeviceScaleFactor::Scale180Percent), + _ => None, + } + } +} + +impl Encode for MonitorLayoutEntry { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let flags = if self.is_primary { + DISPLAYCONTROL_MONITOR_PRIMARY + } else { + 0 + }; + dst.write_u32(flags); + dst.write_i32(self.left); + dst.write_i32(self.top); + dst.write_u32(self.width); + dst.write_u32(self.height); + dst.write_u32(self.physical_width); + dst.write_u32(self.physical_height); + dst.write_u32(self.orientation); + dst.write_u32(self.desktop_scale_factor); + dst.write_u32(self.device_scale_factor); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for MonitorLayoutEntry { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = src.read_u32(); + let left = src.read_i32(); + let top = src.read_i32(); + let width = src.read_u32(); + let height = src.read_u32(); + let physical_width = src.read_u32(); + let physical_height = src.read_u32(); + let orientation = src.read_u32(); + let desktop_scale_factor = src.read_u32(); + let device_scale_factor = src.read_u32(); + + validate_dimensions!(width, height)?; + + Ok(Self { + is_primary: flags & DISPLAYCONTROL_MONITOR_PRIMARY != 0, + left, + top, + width, + height, + physical_width, + physical_height, + orientation, + desktop_scale_factor, + device_scale_factor, + }) + } +} + +/// Valid monitor orientations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MonitorOrientation { + Landscape, + Portrait, + LandscapeFlipped, + PortraitFlipped, +} + +impl MonitorOrientation { + pub fn from_angle(angle: u32) -> Option { + match angle { + 0 => Some(Self::Landscape), + 90 => Some(Self::Portrait), + 180 => Some(Self::LandscapeFlipped), + 270 => Some(Self::PortraitFlipped), + _ => None, + } + } + + pub fn angle(&self) -> u32 { + match self { + Self::Landscape => 0, + Self::Portrait => 90, + Self::LandscapeFlipped => 180, + Self::PortraitFlipped => 270, + } + } +} + +/// Valid device scale factors for monitors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceScaleFactor { + Scale100Percent, + Scale140Percent, + Scale180Percent, +} + +impl DeviceScaleFactor { + pub fn value(&self) -> u32 { + match self { + Self::Scale100Percent => 100, + Self::Scale140Percent => 140, + Self::Scale180Percent => 180, + } + } +} + +fn validate_position(left: i32, top: i32, is_primary: bool) -> EncodeResult<()> { + if is_primary && (left != 0 || top != 0) { + return Err(invalid_field_err!( + "Position", + "Primary monitor position must be (0, 0)" + )); + } + + Ok(()) +} + +fn validate_desktop_scale_factor(desktop_scale_factor: u32) -> EncodeResult<()> { + if !(100..=500).contains(&desktop_scale_factor) { + return Err(invalid_field_err!( + "DesktopScaleFactor", + "Desktop scale factor is out of range" + )); + } + + Ok(()) +} + +fn validate_physical_dimensions(physical_width: u32, physical_height: u32) -> EncodeResult<()> { + if !(10..=10000).contains(&physical_width) { + return Err(invalid_field_err!("PhysicalWidth", "Physical width is out of range")); + } + if !(10..=10000).contains(&physical_height) { + return Err(invalid_field_err!("PhysicalHeight", "Physical height is out of range")); + } + + Ok(()) +} + +fn calculate_monitor_area( + max_num_monitors: u32, + max_monitor_area_factor_a: u32, + max_monitor_area_factor_b: u32, +) -> DecodeResult { + if max_num_monitors > MAX_SUPPORTED_MONITORS.into() { + return Err(invalid_field_err!("NumMonitors", "Too many monitors")); + } + + if max_monitor_area_factor_a > MAX_MONITOR_AREA_FACTOR.into() + || max_monitor_area_factor_b > MAX_MONITOR_AREA_FACTOR.into() + { + return Err(invalid_field_err!( + "MaxMonitorAreaFactor", + "Invalid monitor area factor" + )); + } + + // As per invariants: This multiplication would never overflow. + // 0 <= MAX_MONITOR_AREA_FACTOR * MAX_MONITOR_AREA_FACTOR * MAX_SUPPORTED_MONITORS <= u64::MAX + #[expect(clippy::arithmetic_side_effects)] + Ok(u64::from(max_monitor_area_factor_a) * u64::from(max_monitor_area_factor_b) * u64::from(max_num_monitors)) +} diff --git a/crates/ironrdp-displaycontrol/src/server.rs b/crates/ironrdp-displaycontrol/src/server.rs new file mode 100644 index 00000000..21bee8a2 --- /dev/null +++ b/crates/ironrdp-displaycontrol/src/server.rs @@ -0,0 +1,53 @@ +use ironrdp_core::{decode, impl_as_any}; +use ironrdp_dvc::{DvcMessage, DvcProcessor, DvcServerProcessor}; +use ironrdp_pdu::{decode_err, PduResult}; +use tracing::debug; + +use crate::pdu::{DisplayControlCapabilities, DisplayControlMonitorLayout, DisplayControlPdu}; +use crate::CHANNEL_NAME; + +pub trait DisplayControlHandler: Send { + fn monitor_layout(&self, layout: DisplayControlMonitorLayout) { + debug!(?layout); + } +} + +/// A server for the Display Control Virtual Channel. +pub struct DisplayControlServer { + handler: Box, +} + +impl DisplayControlServer { + /// Create a new DisplayControlServer. + pub fn new(handler: Box) -> Self { + Self { handler } + } +} + +impl_as_any!(DisplayControlServer); + +impl DvcProcessor for DisplayControlServer { + fn channel_name(&self) -> &str { + CHANNEL_NAME + } + + fn start(&mut self, _channel_id: u32) -> PduResult> { + let pdu: DisplayControlPdu = DisplayControlCapabilities::new(1, 3840, 2400) + .map_err(|e| decode_err!(e))? + .into(); + + Ok(vec![Box::new(pdu)]) + } + + fn process(&mut self, _channel_id: u32, payload: &[u8]) -> PduResult> { + match decode(payload).map_err(|e| decode_err!(e))? { + DisplayControlPdu::MonitorLayout(layout) => self.handler.monitor_layout(layout), + DisplayControlPdu::Caps(caps) => { + debug!(?caps); + } + } + Ok(Vec::new()) + } +} + +impl DvcServerProcessor for DisplayControlServer {} diff --git a/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md b/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md new file mode 100644 index 00000000..bd2eda40 --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-pipe-proxy-v0.2.0...ironrdp-dvc-pipe-proxy-v0.2.1)] - 2025-09-24 + +### Bug Fixes + +- Change dvc proxy pipe mode from Message to Byte on Windows (#986) ([5f52a44b84](https://github.com/Devolutions/IronRDP/commit/5f52a44b840dd71eae6a355be00f1c4c671b3b58)) + +- Add blocking logic for sending dvc pipe messages ([3182a018e2](https://github.com/Devolutions/IronRDP/commit/3182a018e2972eb77c52ea248387c96a9eb6a6a6)) + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-pipe-proxy-v0.1.0...ironrdp-dvc-pipe-proxy-v0.2.0)] - 2025-08-29 + +### Features + +- Make dvc named pipe proxy cross-platform (#896) ([166b76010c](https://github.com/Devolutions/IronRDP/commit/166b76010cbd8f8674e6e8d4801fee5cda1ad9e5)) + + - Make dvc named pipe proxy cross-platform (Unix implementation via + `tokio::net::unix::UnixStream`) + - Removed unsafe code for Windows implementation, switched to + `tokio::net::windows::named_pipe` diff --git a/crates/ironrdp-dvc-pipe-proxy/Cargo.toml b/crates/ironrdp-dvc-pipe-proxy/Cargo.toml new file mode 100644 index 00000000..805faf99 --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ironrdp-dvc-pipe-proxy" +version = "0.2.1" +readme = "README.md" +description = "DVC named pipe proxy for IronRDP" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public (PduResult type) +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.4" } +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public (SvcMessage type) + +tracing = { version = "0.1", features = ["log"] } +tokio = { version = "1", features = ["net", "rt", "sync", "macros", "io-util", "fs"]} +async-trait = "0.1" + +[lints] +workspace = true diff --git a/crates/ironrdp-dvc-pipe-proxy/LICENSE-APACHE b/crates/ironrdp-dvc-pipe-proxy/LICENSE-APACHE new file mode 100644 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-dvc-pipe-proxy/LICENSE-MIT b/crates/ironrdp-dvc-pipe-proxy/LICENSE-MIT new file mode 100644 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-dvc-pipe-proxy/README.md b/crates/ironrdp-dvc-pipe-proxy/README.md new file mode 100644 index 00000000..2f20c8a5 --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/README.md @@ -0,0 +1,15 @@ +# IronRDP DVC pipe proxy + +This crate provides a Device Virtual Channel (DVC) handler for IronRDP, enabling proxying of RDP DVC +traffic over a named pipe. + +It was originally designed to simplify custom DVC integration within Devolutions Remote Desktop +Manager (RDM). By implementing a thin pipe proxy for target RDP clients (such as IronRDP, FreeRDP, +mstsc, etc.), the main client logic can be centralized and reused across all supported clients via a +named pipe. + +This approach allows you to implement your DVC logic in one place, making it easier to support +multiple RDP clients without duplicating code. + +Additionally, this crate can be used for other scenarios, such as testing your own custom DVC +channel client, without needing to patch or rebuild IronRDP itself. \ No newline at end of file diff --git a/crates/ironrdp-dvc-pipe-proxy/src/error.rs b/crates/ironrdp-dvc-pipe-proxy/src/error.rs new file mode 100644 index 00000000..9014d010 --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/src/error.rs @@ -0,0 +1,23 @@ +#[derive(Debug)] +pub(crate) enum DvcPipeProxyError { + Io(std::io::Error), + EncodeDvcMessage(ironrdp_core::EncodeError), +} + +impl core::fmt::Display for DvcPipeProxyError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + DvcPipeProxyError::Io(_) => write!(f, "IO error"), + DvcPipeProxyError::EncodeDvcMessage(_) => write!(f, "DVC message encoding error"), + } + } +} + +impl core::error::Error for DvcPipeProxyError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + DvcPipeProxyError::Io(err) => Some(err), + DvcPipeProxyError::EncodeDvcMessage(src) => Some(src), + } + } +} diff --git a/crates/ironrdp-dvc-pipe-proxy/src/lib.rs b/crates/ironrdp-dvc-pipe-proxy/src/lib.rs new file mode 100644 index 00000000..2f89fdd3 --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/src/lib.rs @@ -0,0 +1,11 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +mod error; +mod message; +mod os_pipe; +mod platform; +mod proxy; +mod worker; + +pub use self::proxy::DvcNamedPipeProxy; diff --git a/crates/ironrdp-dvc-pipe-proxy/src/message.rs b/crates/ironrdp-dvc-pipe-proxy/src/message.rs new file mode 100644 index 00000000..3dbb330c --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/src/message.rs @@ -0,0 +1,22 @@ +use ironrdp_core::{ensure_size, Encode, EncodeResult}; +use ironrdp_dvc::DvcEncode; + +pub(crate) struct RawDataDvcMessage(pub Vec); + +impl Encode for RawDataDvcMessage { + fn encode(&self, dst: &mut ironrdp_core::WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_slice(&self.0); + Ok(()) + } + + fn name(&self) -> &'static str { + "RawDataDvcMessage" + } + + fn size(&self) -> usize { + self.0.len() + } +} + +impl DvcEncode for RawDataDvcMessage {} diff --git a/crates/ironrdp-dvc-pipe-proxy/src/os_pipe.rs b/crates/ironrdp-dvc-pipe-proxy/src/os_pipe.rs new file mode 100644 index 00000000..e11d8300 --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/src/os_pipe.rs @@ -0,0 +1,19 @@ +use async_trait::async_trait; + +use crate::error::DvcPipeProxyError; + +#[async_trait] +pub(crate) trait OsPipe: Send + Sync { + /// Creates a new OS pipe and waits for the connection. + async fn connect(pipe_name: &str) -> Result + where + Self: Sized; + + /// Reads data from the pipe and returns the number of bytes read. + /// + /// Returned future should be stateless and can be polled multiple times. + async fn read(&mut self, buffer: &mut [u8]) -> Result; + + /// Writes data to the pipe and returns the number of bytes written. + async fn write_all(&mut self, buffer: &[u8]) -> Result<(), DvcPipeProxyError>; +} diff --git a/crates/ironrdp-dvc-pipe-proxy/src/platform/mod.rs b/crates/ironrdp-dvc-pipe-proxy/src/platform/mod.rs new file mode 100644 index 00000000..346a06db --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/src/platform/mod.rs @@ -0,0 +1,5 @@ +#[cfg(target_os = "windows")] +pub(crate) mod windows; + +#[cfg(not(target_os = "windows"))] +pub(crate) mod unix; diff --git a/crates/ironrdp-dvc-pipe-proxy/src/platform/unix.rs b/crates/ironrdp-dvc-pipe-proxy/src/platform/unix.rs new file mode 100644 index 00000000..b021004f --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/src/platform/unix.rs @@ -0,0 +1,67 @@ +use async_trait::async_trait; +use tokio::fs; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tracing::{info, trace}; + +use crate::error::DvcPipeProxyError; +use crate::os_pipe::OsPipe; + +/// Unix-specific implementation of the OS pipe trait. +pub(crate) struct UnixPipe { + socket: tokio::net::UnixStream, +} + +#[async_trait] +impl OsPipe for UnixPipe { + async fn connect(pipe_name: &str) -> Result { + // Domain socket file could already exist from a previous run. + match fs::metadata(&pipe_name).await { + Ok(metadata) => { + use std::os::unix::fs::FileTypeExt as _; + + info!( + %pipe_name, + "DVC pipe already exists, removing stale file." + ); + + // Just to be sure, check if it's indeed a socket - + // throw an error if calling code accidentally passed a regular file. + if !metadata.file_type().is_socket() { + return Err(DvcPipeProxyError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Path {pipe_name} is not a socket"), + ))); + } + + fs::remove_file(pipe_name).await.map_err(DvcPipeProxyError::Io)?; + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + trace!( + %pipe_name, + "DVC pipe does not exist, creating it." + ); + } + Err(e) => { + return Err(DvcPipeProxyError::Io(e)); + } + } + + let listener = tokio::net::UnixListener::bind(pipe_name).map_err(DvcPipeProxyError::Io)?; + + let (socket, _) = listener.accept().await.map_err(DvcPipeProxyError::Io)?; + + Ok(Self { socket }) + } + + async fn read(&mut self, buffer: &mut [u8]) -> Result { + self.socket.read(buffer).await.map_err(DvcPipeProxyError::Io) + } + + async fn write_all(&mut self, buffer: &[u8]) -> Result<(), DvcPipeProxyError> { + self.socket + .write_all(buffer) + .await + .map_err(DvcPipeProxyError::Io) + .map(|_| ()) + } +} diff --git a/crates/ironrdp-dvc-pipe-proxy/src/platform/windows.rs b/crates/ironrdp-dvc-pipe-proxy/src/platform/windows.rs new file mode 100644 index 00000000..7d40ac32 --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/src/platform/windows.rs @@ -0,0 +1,47 @@ +use async_trait::async_trait; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tokio::net::windows::named_pipe; + +use crate::error::DvcPipeProxyError; +use crate::os_pipe::OsPipe; + +const PIPE_BUFFER_SIZE: u32 = 64 * 1024; + +/// Unix-specific implementation of the OS pipe trait. +pub(crate) struct WindowsPipe { + pipe_server: named_pipe::NamedPipeServer, +} + +#[async_trait] +impl OsPipe for WindowsPipe { + async fn connect(pipe_name: &str) -> Result { + let pipe_name = format!("\\\\.\\pipe\\{pipe_name}"); + + let pipe_server = named_pipe::ServerOptions::new() + .first_pipe_instance(true) + .access_inbound(true) + .access_outbound(true) + .max_instances(2) + .in_buffer_size(PIPE_BUFFER_SIZE) + .out_buffer_size(PIPE_BUFFER_SIZE) + .pipe_mode(named_pipe::PipeMode::Byte) + .create(pipe_name) + .map_err(DvcPipeProxyError::Io)?; + + pipe_server.connect().await.map_err(DvcPipeProxyError::Io)?; + + Ok(Self { pipe_server }) + } + + async fn read(&mut self, buffer: &mut [u8]) -> Result { + self.pipe_server.read(buffer).await.map_err(DvcPipeProxyError::Io) + } + + async fn write_all(&mut self, buffer: &[u8]) -> Result<(), DvcPipeProxyError> { + self.pipe_server + .write_all(buffer) + .await + .map_err(DvcPipeProxyError::Io) + .map(|_| ()) + } +} diff --git a/crates/ironrdp-dvc-pipe-proxy/src/proxy.rs b/crates/ironrdp-dvc-pipe-proxy/src/proxy.rs new file mode 100644 index 00000000..d6b5f44f --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/src/proxy.rs @@ -0,0 +1,128 @@ +use std::sync::{mpsc, Arc}; + +use ironrdp_core::impl_as_any; +use ironrdp_dvc::{DvcClientProcessor, DvcMessage, DvcProcessor}; +use ironrdp_pdu::{pdu_other_err, PduResult}; +use ironrdp_svc::SvcMessage; +use tracing::{debug, info}; + +use crate::worker::{run_worker, OnWriteDvcMessage, WorkerCtx}; + +const IO_MPSC_CHANNEL_SIZE: usize = 100; + +struct WorkerControlCtx { + to_pipe_tx: mpsc::SyncSender>, + abort_event: Arc, +} + +/// A proxy DVC pipe client that forwards DVC messages to/from a named pipe server. +pub struct DvcNamedPipeProxy { + channel_name: String, + named_pipe_name: String, + dvc_write_callback: Option, + worker: Option, +} + +impl DvcNamedPipeProxy { + /// Creates a new DVC named pipe proxy. + /// `dvc_write_callback` is called when the proxy receives a DVC message from the + /// named pipe server and the SVC message is ready to be sent to the DVC channel in the main + /// IronRDP active session loop. + pub fn new(channel_name: &str, named_pipe_name: &str, dvc_write_callback: F) -> Self + where + F: Fn(u32, Vec) -> PduResult<()> + Send + 'static, + { + Self { + channel_name: channel_name.to_owned(), + named_pipe_name: named_pipe_name.to_owned(), + dvc_write_callback: Some(Box::new(dvc_write_callback)), + worker: None, + } + } +} + +impl_as_any!(DvcNamedPipeProxy); + +impl DvcProcessor for DvcNamedPipeProxy { + fn channel_name(&self) -> &str { + &self.channel_name + } + + fn start(&mut self, channel_id: u32) -> PduResult> { + info!(%self.channel_name, %self.named_pipe_name, "Starting DVC named pipe proxy"); + + let on_write_dvc = self + .dvc_write_callback + .take() + .expect("DvcProcessor::start called multiple times"); + + let (to_pipe_tx, to_pipe_rx) = mpsc::sync_channel(IO_MPSC_CHANNEL_SIZE); + + let abort_event = Arc::new(tokio::sync::Notify::new()); + + let ctx = WorkerCtx { + on_write_dvc, + to_pipe_rx, + abort_event: Arc::clone(&abort_event), + pipe_name: self.named_pipe_name.clone(), + channel_name: self.channel_name.clone(), + channel_id, + }; + + self.worker = Some(WorkerControlCtx { + to_pipe_tx, + abort_event, + }); + + #[cfg(not(target_os = "windows"))] + run_worker::(ctx); + + #[cfg(target_os = "windows")] + run_worker::(ctx); + + Ok(vec![]) + } + + fn process(&mut self, _channel_id: u32, payload: &[u8]) -> PduResult> { + if let Some(worker) = &self.worker { + // TODO(@pacmancoder): Whatever buffer size we use here, we will hit buffer limit + // eventually and fail if we are not send it in a blocking manner. + // + // Architecturally, blocking whole IronRDP/async runitme is not ideal (even if we know + // that proxy worker is running on a separate thread and there should be no risk of + // deadlock). + // + // Therefore it is only a temporary solution until we have a better design for DVC + // channels which could block. However its the only way to stop the DVC message flow + // from the host. + // + // During testing, blocking here don't seem to affect performance in any noticeable + // way - there is no visible main RDP functionality slowdown during large IO + // stream transfer. + let result = worker.to_pipe_tx.send(payload.to_vec()); + if let Err(error) = result { + match error { + mpsc::SendError(_) => { + return Err(pdu_other_err!("DVC pipe proxy channel is closed")); + } + } + } + } else { + debug!("Attempt to process DVC packet on non-initialized DVC pipe proxy."); + } + + Ok(vec![]) + } +} + +impl DvcClientProcessor for DvcNamedPipeProxy {} + +impl Drop for DvcNamedPipeProxy { + fn drop(&mut self) { + if let Some(ctx) = &self.worker { + // Signal the worker thread to abort. + ctx.abort_event.notify_one(); + } + self.worker = None; + } +} diff --git a/crates/ironrdp-dvc-pipe-proxy/src/worker.rs b/crates/ironrdp-dvc-pipe-proxy/src/worker.rs new file mode 100644 index 00000000..f0c332ef --- /dev/null +++ b/crates/ironrdp-dvc-pipe-proxy/src/worker.rs @@ -0,0 +1,199 @@ +use std::sync::{mpsc, Arc}; + +use ironrdp_dvc::encode_dvc_messages; +use ironrdp_pdu::PduResult; +use ironrdp_svc::{ChannelFlags, SvcMessage}; +use tokio::sync::Notify; +use tracing::{error, info}; + +use crate::error::DvcPipeProxyError; +use crate::message::RawDataDvcMessage; +use crate::os_pipe::OsPipe; + +const IO_BUFFER_SIZE: usize = 1024 * 64; // 64K + +pub(crate) type OnWriteDvcMessage = Box) -> PduResult<()> + Send>; + +pub(crate) struct WorkerCtx { + pub(crate) on_write_dvc: OnWriteDvcMessage, + pub(crate) to_pipe_rx: mpsc::Receiver>, + pub(crate) abort_event: Arc, + pub(crate) pipe_name: String, + pub(crate) channel_name: String, + pub(crate) channel_id: u32, +} + +pub(crate) fn run_worker(ctx: WorkerCtx) { + let _ = std::thread::spawn(move || { + let channel_name = ctx.channel_name.clone(); + let pipe_name = ctx.pipe_name.clone(); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(DvcPipeProxyError::Io); + + let runtime = match runtime { + Ok(runtime) => runtime, + Err(error) => { + error!( + %channel_name, + %pipe_name, + ?error, + "DVC pipe proxy worker thread initialization failed." + ); + return; + } + }; + + if let Err(error) = runtime.block_on(worker::

(ctx)) { + error!( + %channel_name, + %pipe_name, + ?error, + "DVC pipe proxy worker thread has failed." + ); + } + }); +} + +enum NextWorkerState { + Abort, + Reconnect, +} + +struct BridgedWorkerCtx { + on_write_dvc: OnWriteDvcMessage, + to_pipe_rx: tokio::sync::mpsc::UnboundedReceiver>, + abort_event: Arc, + pipe_name: String, + channel_name: String, + channel_id: u32, +} + +async fn process_client(ctx: &mut BridgedWorkerCtx) -> Result { + let pipe_name = &ctx.pipe_name; + let channel_name = &ctx.channel_name; + + let mut pipe = tokio::select! { + pipe = P::connect(pipe_name) => { + info!(%channel_name, %pipe_name,"DVC proxy worker thread has started."); + pipe? + } + _ = ctx.abort_event.notified() => { + info!(%channel_name, %pipe_name, "DVC proxy worker thread has been aborted."); + return Ok(NextWorkerState::Abort); + } + }; + + let mut from_pipe_buffer = [0u8; IO_BUFFER_SIZE]; + + loop { + let abort = ctx.abort_event.notified(); + let read_pipe = pipe.read(&mut from_pipe_buffer); + let read_dvc = ctx.to_pipe_rx.recv(); + + tokio::select! { + () = abort => { + info!(%channel_name, %pipe_name, "Received abort signal for DVC proxy worker thread."); + return Ok(NextWorkerState::Abort); + } + read_bytes_result = read_pipe => { + let read_bytes = read_bytes_result?; + + if read_bytes == 0 { + info!(%channel_name, %pipe_name, "DVC proxy pipe returned EOF"); + + // If client unexpectedly closed the connection, we should + // still be able to reconnect to same session. + return Ok(NextWorkerState::Reconnect); + } + + let messages = encode_dvc_messages( + ctx.channel_id, + vec![Box::new(RawDataDvcMessage(from_pipe_buffer[..read_bytes].to_vec()))], + ChannelFlags::empty(), + ) + .map_err(DvcPipeProxyError::EncodeDvcMessage)?; + + if let Err(error) = (ctx.on_write_dvc)(0, messages) { + error!(%channel_name, %pipe_name, ?error, "DVC pipe proxy write callback failed"); + } + } + dvc_input = read_dvc => { + let data = match dvc_input { + Some(data) => data, + None => { + info!(%channel_name, %pipe_name, "DVC mpsc channel returned EOF."); + // Server DVC has been closed, there is no point in + // trying to reconnect. + return Ok(NextWorkerState::Abort); + } + }; + + if let Err(error) = pipe.write_all(&data).await + { + error!(%channel_name, %pipe_name, ?error, "Failed to write to DVC pipe"); + continue; + } + } + }; + } +} + +async fn worker(ctx: WorkerCtx) -> Result<(), DvcPipeProxyError> { + // Create a bridge between std::sync::mpsc and tokio for async compatibility. + // It is fine to use unbounded channel here because we are using it only to + // forward data from a bounded channel (with size IO_MPSC_CHANNEL_SIZE), + // so we will never have unbounded memory growth. + let (async_tx, async_rx) = tokio::sync::mpsc::unbounded_channel(); + + let WorkerCtx { + on_write_dvc, + to_pipe_rx: std_rx, + abort_event, + pipe_name, + channel_name, + channel_id, + } = ctx; + + // Spawn a thread to bridge std::sync::mpsc to tokio::sync::mpsc. + std::thread::spawn(move || { + while let Ok(data) = std_rx.recv() { + if async_tx.send(data).is_err() { + break; // Receiver dropped + } + } + }); + + let mut bridged_ctx = BridgedWorkerCtx { + on_write_dvc, + to_pipe_rx: async_rx, + abort_event, + pipe_name, + channel_name, + channel_id, + }; + loop { + match process_client::

(&mut bridged_ctx).await? { + NextWorkerState::Abort => { + info!( + channel_name = %bridged_ctx.channel_name, + pipe_name = %bridged_ctx.pipe_name, + "Aborting DVC proxy worker thread." + ); + break; + } + NextWorkerState::Reconnect => { + info!( + channel_name = %bridged_ctx.channel_name, + pipe_name = %bridged_ctx.pipe_name, + "Reconnecting to DVC pipe..." + ); + continue; + } + }; + } + + Ok(()) +} diff --git a/crates/ironrdp-dvc/CHANGELOG.md b/crates/ironrdp-dvc/CHANGELOG.md new file mode 100644 index 00000000..6eb3c6a5 --- /dev/null +++ b/crates/ironrdp-dvc/CHANGELOG.md @@ -0,0 +1,51 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.4.0...ironrdp-dvc-v0.4.1)] - 2025-09-04 + +### Features + +- Add API to attach dynamic channels to an already created `DrdynvcClient` instance (#938) ([17833fe009](https://github.com/Devolutions/IronRDP/commit/17833fe009279823c4076d3e2e0c7d063fd24a43)) + +## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.3.0...ironrdp-dvc-v0.3.1)] - 2025-06-27 + +### Features + +- Add `DynamicChannelSet::get_by_channel_id` (#791) ([5482365655](https://github.com/Devolutions/IronRDP/commit/5482365655e5c171cd967eda401b01161a9f6602)) + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.1.3...ironrdp-dvc-v0.2.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.1.2...ironrdp-dvc-v0.1.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.1.1...ironrdp-dvc-v0.1.2)] - 2025-01-28 + +### Features + +- Some debug statement on invalid channel state ([265b661b81](https://github.com/Devolutions/IronRDP/commit/265b661b81af19860c4564ba35ad22564f61cd02)) + +- Add CreationStatus::NOT_FOUND ([ab8a87d942](https://github.com/Devolutions/IronRDP/commit/ab8a87d94259a4e1df5f3a2a8d4c592377857b21)) + + For completeness, this error is used by FreeRDP. + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.1.0...ironrdp-dvc-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-dvc/Cargo.toml b/crates/ironrdp-dvc/Cargo.toml new file mode 100644 index 00000000..c0d17f9b --- /dev/null +++ b/crates/ironrdp-dvc/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "ironrdp-dvc" +version = "0.4.1" +readme = "README.md" +description = "DRDYNVC static channel implementation and traits to implement dynamic virtual channels" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +std = [] + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6", features = ["alloc"] } # public +tracing = { version = "0.1", features = ["log"] } +slab = "0.4" + +[lints] +workspace = true diff --git a/crates/ironrdp-dvc/LICENSE-APACHE b/crates/ironrdp-dvc/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-dvc/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-dvc/LICENSE-MIT b/crates/ironrdp-dvc/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-dvc/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-dvc/README.md b/crates/ironrdp-dvc/README.md new file mode 100644 index 00000000..c8218d4d --- /dev/null +++ b/crates/ironrdp-dvc/README.md @@ -0,0 +1,7 @@ +# IronRDP DVC + +RDP Dynamic Virtual Channel (DVC) support. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-dvc/src/client.rs b/crates/ironrdp-dvc/src/client.rs new file mode 100644 index 00000000..ed383d53 --- /dev/null +++ b/crates/ironrdp-dvc/src/client.rs @@ -0,0 +1,189 @@ +use alloc::vec::Vec; +use core::any::TypeId; +use core::fmt; + +use ironrdp_core::{impl_as_any, Decode as _, DecodeResult, ReadCursor}; +use ironrdp_pdu::{self as pdu, decode_err, encode_err, pdu_other_err}; +use ironrdp_svc::{ChannelFlags, CompressionCondition, SvcClientProcessor, SvcMessage, SvcProcessor}; +use pdu::gcc::ChannelName; +use pdu::PduResult; +use tracing::debug; + +use crate::pdu::{ + CapabilitiesResponsePdu, CapsVersion, ClosePdu, CreateResponsePdu, CreationStatus, DrdynvcClientPdu, + DrdynvcServerPdu, +}; +use crate::{encode_dvc_messages, DvcProcessor, DynamicChannelSet, DynamicVirtualChannel}; + +pub trait DvcClientProcessor: DvcProcessor {} + +/// DRDYNVC Static Virtual Channel (the Remote Desktop Protocol: Dynamic Virtual Channel Extension) +/// +/// It adds support for dynamic virtual channels (DVC). +pub struct DrdynvcClient { + dynamic_channels: DynamicChannelSet, + /// Indicates whether the capability request/response handshake has been completed. + cap_handshake_done: bool, +} + +impl fmt::Debug for DrdynvcClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DrdynvcClient([")?; + + for (i, channel) in self.dynamic_channels.values().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", channel.channel_name())?; + } + + write!(f, "])") + } +} + +impl DrdynvcClient { + pub const NAME: ChannelName = ChannelName::from_static(b"drdynvc\0"); + + pub fn new() -> Self { + Self { + dynamic_channels: DynamicChannelSet::new(), + cap_handshake_done: false, + } + } + + // FIXME(#61): it’s likely we want to enable adding dynamic channels at any point during the session (message passing? other approach?) + + #[must_use] + pub fn with_dynamic_channel(mut self, channel: T) -> Self + where + T: DvcProcessor + 'static, + { + self.dynamic_channels.insert(channel); + self + } + + pub fn attach_dynamic_channel(&mut self, channel: T) + where + T: DvcProcessor + 'static, + { + self.dynamic_channels.insert(channel); + } + + pub fn get_dvc_by_type_id(&self) -> Option<&DynamicVirtualChannel> + where + T: DvcProcessor, + { + self.dynamic_channels.get_by_type_id(TypeId::of::()) + } + + pub fn get_dvc_by_channel_id(&self, channel_id: u32) -> Option<&DynamicVirtualChannel> { + self.dynamic_channels.get_by_channel_id(channel_id) + } + + fn create_capabilities_response(&mut self) -> SvcMessage { + let caps_response = DrdynvcClientPdu::Capabilities(CapabilitiesResponsePdu::new(CapsVersion::V1)); + debug!("Send DVC Capabilities Response PDU: {caps_response:?}"); + self.cap_handshake_done = true; + SvcMessage::from(caps_response) + } +} + +impl_as_any!(DrdynvcClient); + +impl Default for DrdynvcClient { + fn default() -> Self { + Self::new() + } +} + +impl SvcProcessor for DrdynvcClient { + fn channel_name(&self) -> ChannelName { + DrdynvcClient::NAME + } + + fn compression_condition(&self) -> CompressionCondition { + CompressionCondition::WhenRdpDataIsCompressed + } + + fn process(&mut self, payload: &[u8]) -> PduResult> { + let pdu = decode_dvc_message(payload).map_err(|e| decode_err!(e))?; + let mut responses = Vec::new(); + + match pdu { + DrdynvcServerPdu::Capabilities(caps_request) => { + debug!("Got DVC Capabilities Request PDU: {caps_request:?}"); + responses.push(self.create_capabilities_response()); + } + DrdynvcServerPdu::Create(create_request) => { + debug!("Got DVC Create Request PDU: {create_request:?}"); + let channel_id = create_request.channel_id(); + let channel_name = create_request.into_channel_name(); + + if !self.cap_handshake_done { + debug!( + "Got DVC Create Request PDU before a Capabilities Request PDU. \ + Sending Capabilities Response PDU before the Create Response PDU." + ); + responses.push(self.create_capabilities_response()); + } + + let channel_exists = self.dynamic_channels.get_by_channel_name(&channel_name).is_some(); + let (creation_status, start_messages) = if channel_exists { + // If we have a handler for this channel, attach the channel ID + // and get any start messages. + self.dynamic_channels + .attach_channel_id(channel_name.clone(), channel_id); + let dynamic_channel = self + .dynamic_channels + .get_by_channel_name_mut(&channel_name) + .expect("channel exists"); + (CreationStatus::OK, dynamic_channel.start()?) + } else { + (CreationStatus::NO_LISTENER, Vec::new()) + }; + + let create_response = DrdynvcClientPdu::Create(CreateResponsePdu::new(channel_id, creation_status)); + debug!("Send DVC Create Response PDU: {create_response:?}"); + responses.push(SvcMessage::from(create_response)); + + // If this DVC has start messages, send them. + if !start_messages.is_empty() { + responses.extend( + encode_dvc_messages(channel_id, start_messages, ChannelFlags::empty()) + .map_err(|e| encode_err!(e))?, + ); + } + } + DrdynvcServerPdu::Close(close_request) => { + debug!("Got DVC Close Request PDU: {close_request:?}"); + self.dynamic_channels.remove_by_channel_id(close_request.channel_id()); + + let close_response = DrdynvcClientPdu::Close(ClosePdu::new(close_request.channel_id())); + + debug!("Send DVC Close Response PDU: {close_response:?}"); + responses.push(SvcMessage::from(close_response)); + } + DrdynvcServerPdu::Data(data) => { + let channel_id = data.channel_id(); + + let messages = self + .dynamic_channels + .get_by_channel_id_mut(channel_id) + .ok_or_else(|| pdu_other_err!("access to non existing DVC channel"))? + .process(data)?; + + responses.extend( + encode_dvc_messages(channel_id, messages, ChannelFlags::empty()).map_err(|e| encode_err!(e))?, + ); + } + } + + Ok(responses) + } +} + +impl SvcClientProcessor for DrdynvcClient {} + +fn decode_dvc_message(user_data: &[u8]) -> DecodeResult { + DrdynvcServerPdu::decode(&mut ReadCursor::new(user_data)) +} diff --git a/crates/ironrdp-dvc/src/complete_data.rs b/crates/ironrdp-dvc/src/complete_data.rs new file mode 100644 index 00000000..d910aa4d --- /dev/null +++ b/crates/ironrdp-dvc/src/complete_data.rs @@ -0,0 +1,81 @@ +use alloc::vec::Vec; +use core::cmp; + +use ironrdp_core::{cast_length, invalid_field_err, DecodeResult}; +use tracing::error; + +use crate::pdu::{DataFirstPdu, DataPdu, DrdynvcDataPdu}; + +#[derive(Debug, PartialEq)] +pub(crate) struct CompleteData { + total_size: usize, + data: Vec, +} + +impl CompleteData { + pub(crate) fn new() -> Self { + Self { + total_size: 0, + data: Vec::new(), + } + } + + pub(crate) fn process_data(&mut self, pdu: DrdynvcDataPdu) -> DecodeResult>> { + match pdu { + DrdynvcDataPdu::DataFirst(data_first) => self.process_data_first_pdu(data_first), + DrdynvcDataPdu::Data(data) => self.process_data_pdu(data), + } + } + + fn process_data_first_pdu(&mut self, data_first: DataFirstPdu) -> DecodeResult>> { + let total_data_size: DecodeResult<_> = cast_length!("DataFirstPdu::length", data_first.length()); + let total_data_size = total_data_size?; + if self.total_size != 0 || !self.data.is_empty() { + error!("Incomplete DVC message, it will be skipped"); + + self.data.clear(); + } + + if total_data_size == data_first.data().len() { + Ok(Some(data_first.into_data())) + } else { + self.total_size = total_data_size; + self.data = data_first.into_data(); + + Ok(None) + } + } + + fn process_data_pdu(&mut self, mut data: DataPdu) -> DecodeResult>> { + if self.total_size == 0 && self.data.is_empty() { + // message is not fragmented + return Ok(Some(data.into_data())); + } + + // The message is fragmented and needs to be reassembled. + match self.data.len().checked_add(data.data().len()) { + Some(actual_data_length) => { + match actual_data_length.cmp(&(self.total_size)) { + cmp::Ordering::Less => { + // this is one of the fragmented messages, just append it + self.data.append(data.data_mut()); + Ok(None) + } + cmp::Ordering::Equal => { + // this is the last fragmented message, need to return the whole reassembled message + self.total_size = 0; + self.data.append(data.data_mut()); + Ok(Some(self.data.drain(..).collect())) + } + cmp::Ordering::Greater => { + error!("Actual DVC message size is grater than expected total DVC message size"); + self.total_size = 0; + self.data.clear(); + Ok(None) + } + } + } + _ => Err(invalid_field_err!("DVC message", "data", "overflow occurred")), + } + } +} diff --git a/crates/ironrdp-dvc/src/lib.rs b/crates/ironrdp-dvc/src/lib.rs new file mode 100644 index 00000000..7baa918a --- /dev/null +++ b/crates/ironrdp-dvc/src/lib.rs @@ -0,0 +1,232 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::boxed::Box; +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; +use core::any::TypeId; + +use pdu::DrdynvcDataPdu; + +use crate::alloc::borrow::ToOwned as _; +// Re-export ironrdp_pdu crate for convenience +#[rustfmt::skip] // do not re-order this pub use +pub use ironrdp_pdu; +use ironrdp_core::{assert_obj_safe, cast_length, encode_vec, other_err, AsAny, Encode, EncodeResult}; +use ironrdp_pdu::{decode_err, pdu_other_err, PduResult}; +use ironrdp_svc::SvcMessage; + +mod complete_data; +use complete_data::CompleteData; + +mod client; +pub use client::*; + +mod server; +pub use server::*; + +pub mod pdu; + +/// Represents a message that, when encoded, forms a complete PDU for a given dynamic virtual channel. +/// This means a message that is ready to be wrapped in [`pdu::DataFirstPdu`] and [`pdu::DataPdu`] PDUs +/// (being split into multiple of such PDUs if necessary). +pub trait DvcEncode: Encode + Send {} +pub type DvcMessage = Box; + +/// A type that is a Dynamic Virtual Channel (DVC) +/// +/// Dynamic virtual channels may be created at any point during the RDP session. +/// The Dynamic Virtual Channel APIs exist to address limitations of Static Virtual Channels: +/// - Limited number of channels +/// - Packet reconstruction +pub trait DvcProcessor: AsAny + Send { + /// The name of the channel, e.g. "Microsoft::Windows::RDS::DisplayControl" + fn channel_name(&self) -> &str; + + /// Returns any messages that should be sent immediately + /// upon the channel being created. + fn start(&mut self, channel_id: u32) -> PduResult>; + + fn process(&mut self, channel_id: u32, payload: &[u8]) -> PduResult>; + + fn close(&mut self, _channel_id: u32) {} +} + +assert_obj_safe!(DvcProcessor); + +pub fn encode_dvc_messages( + channel_id: u32, + messages: Vec, + flags: ironrdp_svc::ChannelFlags, +) -> EncodeResult> { + let mut res = Vec::new(); + for msg in messages { + let total_length = msg.size(); + let needs_splitting = total_length >= DrdynvcDataPdu::MAX_DATA_SIZE; + + let msg = encode_vec(msg.as_ref())?; + let mut off = 0; + + while off < total_length { + let first = off == 0; + + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked underflow)")] + let remaining_length = total_length.checked_sub(off).expect("never overflow"); + let size = core::cmp::min(remaining_length, DrdynvcDataPdu::MAX_DATA_SIZE); + let end = off + .checked_add(size) + .ok_or_else(|| other_err!("encode_dvc_messages", "overflow occurred"))?; + + let pdu = if needs_splitting && first { + DrdynvcDataPdu::DataFirst(pdu::DataFirstPdu::new( + channel_id, + cast_length!("total_length", total_length)?, + msg[off..end].to_vec(), + )) + } else { + DrdynvcDataPdu::Data(pdu::DataPdu::new(channel_id, msg[off..end].to_vec())) + }; + + let svc = SvcMessage::from(pdu).with_flags(flags); + + res.push(svc); + off = end; + } + } + + Ok(res) +} + +pub struct DynamicVirtualChannel { + channel_processor: Box, + complete_data: CompleteData, + /// The channel ID assigned by the server. + /// + /// This field is `None` until the server assigns a channel ID. + channel_id: Option, +} + +impl DynamicVirtualChannel { + fn new(handler: T) -> Self { + Self { + channel_processor: Box::new(handler), + complete_data: CompleteData::new(), + channel_id: None, + } + } + + pub fn is_open(&self) -> bool { + self.channel_id.is_some() + } + + pub fn channel_id(&self) -> Option { + self.channel_id + } + + pub fn channel_processor_downcast_ref(&self) -> Option<&T> { + self.channel_processor.as_any().downcast_ref() + } + + fn start(&mut self) -> PduResult> { + if let Some(channel_id) = self.channel_id { + self.channel_processor.start(channel_id) + } else { + Err(pdu_other_err!("DynamicVirtualChannel::start", "channel ID not set")) + } + } + + fn process(&mut self, pdu: DrdynvcDataPdu) -> PduResult> { + let channel_id = pdu.channel_id(); + let complete_data = self.complete_data.process_data(pdu).map_err(|e| decode_err!(e))?; + if let Some(complete_data) = complete_data { + self.channel_processor.process(channel_id, &complete_data) + } else { + Ok(Vec::new()) + } + } + + fn channel_name(&self) -> &str { + self.channel_processor.channel_name() + } +} + +struct DynamicChannelSet { + channels: BTreeMap, + name_to_channel_id: BTreeMap, + channel_id_to_name: BTreeMap, + type_id_to_name: BTreeMap, +} + +impl DynamicChannelSet { + #[inline] + fn new() -> Self { + Self { + channels: BTreeMap::new(), + name_to_channel_id: BTreeMap::new(), + channel_id_to_name: BTreeMap::new(), + type_id_to_name: BTreeMap::new(), + } + } + + fn insert(&mut self, channel: T) -> Option { + let name = channel.channel_name().to_owned(); + self.type_id_to_name.insert(TypeId::of::(), name.clone()); + self.channels.insert(name, DynamicVirtualChannel::new(channel)) + } + + fn attach_channel_id(&mut self, name: DynamicChannelName, id: DynamicChannelId) -> Option { + self.channel_id_to_name.insert(id, name.clone()); + self.name_to_channel_id.insert(name.clone(), id); + let dvc = self.get_by_channel_name_mut(&name)?; + let old_id = dvc.channel_id; + dvc.channel_id = Some(id); + old_id + } + + fn get_by_type_id(&self, type_id: TypeId) -> Option<&DynamicVirtualChannel> { + self.type_id_to_name + .get(&type_id) + .and_then(|name| self.channels.get(name)) + } + + fn get_by_channel_name(&self, name: &DynamicChannelName) -> Option<&DynamicVirtualChannel> { + self.channels.get(name) + } + + fn get_by_channel_name_mut(&mut self, name: &DynamicChannelName) -> Option<&mut DynamicVirtualChannel> { + self.channels.get_mut(name) + } + + fn get_by_channel_id(&self, id: DynamicChannelId) -> Option<&DynamicVirtualChannel> { + self.channel_id_to_name + .get(&id) + .and_then(|name| self.channels.get(name)) + } + + fn get_by_channel_id_mut(&mut self, id: DynamicChannelId) -> Option<&mut DynamicVirtualChannel> { + self.channel_id_to_name + .get(&id) + .and_then(|name| self.channels.get_mut(name)) + } + + fn remove_by_channel_id(&mut self, id: DynamicChannelId) -> Option { + if let Some(name) = self.channel_id_to_name.remove(&id) { + return self.name_to_channel_id.remove(&name); + // Channels are retained in the `self.channels` and `self.type_id_to_name` map to allow potential + // dynamic re-addition by the server. + } + None + } + + #[inline] + fn values(&self) -> impl Iterator { + self.channels.values() + } +} + +pub type DynamicChannelName = String; +pub type DynamicChannelId = u32; diff --git a/crates/ironrdp-dvc/src/pdu.rs b/crates/ironrdp-dvc/src/pdu.rs new file mode 100644 index 00000000..3f0c461e --- /dev/null +++ b/crates/ironrdp-dvc/src/pdu.rs @@ -0,0 +1,910 @@ +use alloc::format; +use core::fmt; + +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, unsupported_value_err, Decode, DecodeError, + DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use ironrdp_pdu::utils::{ + checked_sum, encoded_str_len, read_string_from_cursor, strict_sum, write_string_to_cursor, CharacterSet, +}; +use ironrdp_svc::SvcEncode; + +use crate::{DynamicChannelId, String, Vec}; + +/// Dynamic Virtual Channel PDU's that are sent by both client and server. +#[derive(Debug, PartialEq)] +pub enum DrdynvcDataPdu { + DataFirst(DataFirstPdu), + Data(DataPdu), +} + +impl DrdynvcDataPdu { + /// Maximum size of the `data` field in `DrdynvcDataPdu`. + pub const MAX_DATA_SIZE: usize = 1590; + + pub fn channel_id(&self) -> DynamicChannelId { + match self { + DrdynvcDataPdu::DataFirst(pdu) => pdu.channel_id, + DrdynvcDataPdu::Data(pdu) => pdu.channel_id, + } + } +} + +impl Encode for DrdynvcDataPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + DrdynvcDataPdu::DataFirst(pdu) => pdu.encode(dst), + DrdynvcDataPdu::Data(pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + match self { + DrdynvcDataPdu::DataFirst(_) => DataFirstPdu::name(), + DrdynvcDataPdu::Data(_) => DataPdu::name(), + } + } + + fn size(&self) -> usize { + match self { + DrdynvcDataPdu::DataFirst(pdu) => pdu.size(), + DrdynvcDataPdu::Data(pdu) => pdu.size(), + } + } +} + +/// Dynamic Virtual Channel PDU's that are sent by the client. +#[derive(Debug, PartialEq)] +pub enum DrdynvcClientPdu { + Capabilities(CapabilitiesResponsePdu), + Create(CreateResponsePdu), + Close(ClosePdu), + Data(DrdynvcDataPdu), +} + +impl Encode for DrdynvcClientPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + DrdynvcClientPdu::Capabilities(pdu) => pdu.encode(dst), + DrdynvcClientPdu::Create(pdu) => pdu.encode(dst), + DrdynvcClientPdu::Data(pdu) => pdu.encode(dst), + DrdynvcClientPdu::Close(pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + match self { + DrdynvcClientPdu::Capabilities(_) => CapabilitiesResponsePdu::name(), + DrdynvcClientPdu::Create(_) => CreateResponsePdu::name(), + DrdynvcClientPdu::Data(pdu) => pdu.name(), + DrdynvcClientPdu::Close(_) => ClosePdu::name(), + } + } + + fn size(&self) -> usize { + match self { + DrdynvcClientPdu::Capabilities(_) => CapabilitiesResponsePdu::size(), + DrdynvcClientPdu::Create(pdu) => pdu.size(), + DrdynvcClientPdu::Data(pdu) => pdu.size(), + DrdynvcClientPdu::Close(pdu) => pdu.size(), + } + } +} + +impl Decode<'_> for DrdynvcClientPdu { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + let header = Header::decode(src)?; + match header.cmd { + Cmd::Create => Ok(Self::Create(CreateResponsePdu::decode(header, src)?)), + Cmd::DataFirst => Ok(Self::Data(DrdynvcDataPdu::DataFirst(DataFirstPdu::decode( + header, src, + )?))), + Cmd::Data => Ok(Self::Data(DrdynvcDataPdu::Data(DataPdu::decode(header, src)?))), + Cmd::Close => Ok(Self::Close(ClosePdu::decode(header, src)?)), + Cmd::Capability => Ok(Self::Capabilities(CapabilitiesResponsePdu::decode(header, src)?)), + _ => Err(unsupported_value_err!("Cmd", header.cmd.into())), + } + } +} + +/// Dynamic Virtual Channel PDU's that are sent by the server. +#[derive(Debug, PartialEq)] +pub enum DrdynvcServerPdu { + Capabilities(CapabilitiesRequestPdu), + Create(CreateRequestPdu), + Close(ClosePdu), + Data(DrdynvcDataPdu), +} + +impl Encode for DrdynvcServerPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + DrdynvcServerPdu::Data(pdu) => pdu.encode(dst), + DrdynvcServerPdu::Capabilities(pdu) => pdu.encode(dst), + DrdynvcServerPdu::Create(pdu) => pdu.encode(dst), + DrdynvcServerPdu::Close(pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + match self { + DrdynvcServerPdu::Data(pdu) => pdu.name(), + DrdynvcServerPdu::Capabilities(pdu) => pdu.name(), + DrdynvcServerPdu::Create(_) => CreateRequestPdu::name(), + DrdynvcServerPdu::Close(_) => ClosePdu::name(), + } + } + + fn size(&self) -> usize { + match self { + DrdynvcServerPdu::Data(pdu) => pdu.size(), + DrdynvcServerPdu::Capabilities(pdu) => pdu.size(), + DrdynvcServerPdu::Create(pdu) => pdu.size(), + DrdynvcServerPdu::Close(pdu) => pdu.size(), + } + } +} + +impl Decode<'_> for DrdynvcServerPdu { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + let header = Header::decode(src)?; + match header.cmd { + Cmd::Create => Ok(Self::Create(CreateRequestPdu::decode(header, src)?)), + Cmd::DataFirst => Ok(Self::Data(DrdynvcDataPdu::DataFirst(DataFirstPdu::decode( + header, src, + )?))), + Cmd::Data => Ok(Self::Data(DrdynvcDataPdu::Data(DataPdu::decode(header, src)?))), + Cmd::Close => Ok(Self::Close(ClosePdu::decode(header, src)?)), + Cmd::Capability => Ok(Self::Capabilities(CapabilitiesRequestPdu::decode(header, src)?)), + _ => Err(unsupported_value_err!("Cmd", header.cmd.into())), + } + } +} + +// Dynamic virtual channel PDU's are sent over a static virtual channel, so they are `SvcEncode`. +impl SvcEncode for DrdynvcDataPdu {} +impl SvcEncode for DrdynvcClientPdu {} +impl SvcEncode for DrdynvcServerPdu {} + +/// [2.2] Message Syntax +/// +/// [2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/0b07a750-bf51-4042-bcf2-a991b6729d6e +#[derive(Debug, PartialEq)] +pub struct Header { + cb_id: FieldType, // 2 bit + sp: FieldType, // 2 bit; meaning depends on the cmd field + cmd: Cmd, // 4 bit +} + +impl Header { + pub const FIXED_PART_SIZE: usize = 1; + /// Create a new `Header` with the given `cb_id_val`, `sp_val`, and `cmd`. + /// + /// If `cb_id_val` or `sp_val` is not relevant for a given `cmd`, it should be set to 0 respectively. + fn new(cb_id_val: u32, sp_val: u32, cmd: Cmd) -> Self { + Self { + cb_id: FieldType::for_val(cb_id_val), + sp: FieldType::for_val(sp_val), + cmd, + } + } + + fn with_cb_id(self, cb_id: FieldType) -> Self { + Self { cb_id, ..self } + } + + fn with_sp(self, sp: FieldType) -> Self { + Self { sp, ..self } + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + dst.write_u8(((self.cmd.as_u8()) << 4) | (Into::::into(self.sp) << 2) | Into::::into(self.cb_id)); + Ok(()) + } + + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::size()); + let byte = src.read_u8(); + let cmd = Cmd::try_from(byte >> 4)?; + let sp = FieldType::from((byte >> 2) & 0b11); + let cb_id = FieldType::from(byte & 0b11); + Ok(Self { cb_id, sp, cmd }) + } + + fn size() -> usize { + Self::FIXED_PART_SIZE + } +} + +/// [2.2] Message Syntax +/// +/// [2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/0b07a750-bf51-4042-bcf2-a991b6729d6e +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq)] +enum Cmd { + Create = 0x01, + DataFirst = 0x02, + Data = 0x03, + Close = 0x04, + Capability = 0x05, + DataFirstCompressed = 0x06, + DataCompressed = 0x07, + SoftSyncRequest = 0x08, + SoftSyncResponse = 0x09, +} + +impl Cmd { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +impl TryFrom for Cmd { + type Error = DecodeError; + + fn try_from(byte: u8) -> Result { + match byte { + 0x01 => Ok(Self::Create), + 0x02 => Ok(Self::DataFirst), + 0x03 => Ok(Self::Data), + 0x04 => Ok(Self::Close), + 0x05 => Ok(Self::Capability), + 0x06 => Ok(Self::DataFirstCompressed), + 0x07 => Ok(Self::DataCompressed), + 0x08 => Ok(Self::SoftSyncRequest), + 0x09 => Ok(Self::SoftSyncResponse), + _ => Err(invalid_field_err!("Cmd", "invalid cmd")), + } + } +} + +impl fmt::Display for Cmd { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Cmd::Create => "Create", + Cmd::DataFirst => "DataFirst", + Cmd::Data => "Data", + Cmd::Close => "Close", + Cmd::Capability => "Capability", + Cmd::DataFirstCompressed => "DataFirstCompressed", + Cmd::DataCompressed => "DataCompressed", + Cmd::SoftSyncRequest => "SoftSyncRequest", + Cmd::SoftSyncResponse => "SoftSyncResponse", + }) + } +} + +impl From for String { + fn from(cmd: Cmd) -> Self { + format!("{cmd:?}") + } +} + +/// 2.2.3.1 DVC Data First PDU (DYNVC_DATA_FIRST) +/// +/// [2.2.3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/69377767-56a6-4ab8-996b-7758676e9261 +#[derive(Debug, PartialEq)] +pub struct DataFirstPdu { + header: Header, + channel_id: DynamicChannelId, + /// Length is the *total* length of the data to be sent, including the length + /// of the data that will be sent by subsequent DVC_DATA PDUs. + length: u32, + /// Data is just the data to be sent in this PDU. + data: Vec, +} + +impl DataFirstPdu { + /// Create a new `DataFirstPdu` with the given `channel_id`, `length`, and `data`. + /// + /// `length` is the *total* length of the data to be sent, including the length + /// of the data that will be sent by subsequent `DataPdu`s. + /// + /// `data` is just the data to be sent in this PDU. + pub fn new(channel_id: DynamicChannelId, total_length: u32, data: Vec) -> Self { + Self { + header: Header::new(channel_id, total_length, Cmd::DataFirst), + channel_id, + length: total_length, + data, + } + } + + #[must_use] + pub fn with_cb_id_type(self, cb_id: FieldType) -> Self { + Self { + header: self.header.with_cb_id(cb_id), + ..self + } + } + + #[must_use] + pub fn with_sp_type(self, sp: FieldType) -> Self { + Self { + header: self.header.with_sp(sp), + ..self + } + } + + pub fn length(&self) -> u32 { + self.length + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub fn into_data(self) -> Vec { + self.data + } + + fn decode(header: Header, src: &mut ReadCursor<'_>) -> DecodeResult { + let fixed_part_size = checked_sum(&[header.cb_id.size_of_val(), header.sp.size_of_val()])?; + ensure_size!(in: src, size: fixed_part_size); + let channel_id = header.cb_id.decode_val(src)?; + let length = header.sp.decode_val(src)?; + let data = src.read_remaining().to_vec(); + Ok(Self { + header, + channel_id, + length, + data, + }) + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.header.encode(dst)?; + self.header.cb_id.encode_val(self.channel_id, dst)?; + self.header + .sp + .encode_val(cast_length!("DataFirstPdu::Length", self.length)?, dst)?; + dst.write_slice(&self.data); + Ok(()) + } + + fn name() -> &'static str { + "DYNVC_DATA_FIRST" + } + + fn size(&self) -> usize { + strict_sum(&[ + Header::size(), + self.header.cb_id.size_of_val(), + self.header.sp.size_of_val(), + self.data.len(), + ]) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct FieldType(u8); + +impl FieldType { + pub const U8: Self = Self(0x00); + pub const U16: Self = Self(0x01); + pub const U32: Self = Self(0x02); + + fn encode_val(&self, value: u32, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size_of_val()); + match *self { + FieldType::U8 => dst.write_u8(cast_length!("FieldType::encode", value)?), + FieldType::U16 => dst.write_u16(cast_length!("FieldType::encode", value)?), + FieldType::U32 => dst.write_u32(value), + _ => return Err(invalid_field_err!("FieldType", "invalid field type")), + }; + Ok(()) + } + + fn decode_val(&self, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: self.size_of_val()); + match *self { + FieldType::U8 => Ok(u32::from(src.read_u8())), + FieldType::U16 => Ok(u32::from(src.read_u16())), + FieldType::U32 => Ok(src.read_u32()), + _ => Err(invalid_field_err!("FieldType", "invalid field type")), + } + } + + /// Returns the size of the value in bytes. + fn size_of_val(&self) -> usize { + match *self { + FieldType::U8 => 1, + FieldType::U16 => 2, + FieldType::U32 => 4, + _ => 0, + } + } + + fn for_val(value: u32) -> Self { + if u8::try_from(value).is_ok() { + FieldType::U8 + } else if u16::try_from(value).is_ok() { + FieldType::U16 + } else { + FieldType::U32 + } + } +} + +impl From for FieldType { + fn from(byte: u8) -> Self { + match byte { + 0x00 => Self::U8, + 0x01 => Self::U16, + 0x02 => Self::U32, + _ => Self(byte), + } + } +} + +impl From for u8 { + fn from(field_type: FieldType) -> Self { + field_type.0 + } +} + +/// 2.2.3.2 DVC Data PDU (DYNVC_DATA) +/// +/// [2.2.3.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/15b59886-db44-47f1-8da3-47c8fcd82803 +#[derive(Debug, PartialEq)] +pub struct DataPdu { + header: Header, + channel_id: DynamicChannelId, + data: Vec, +} + +impl DataPdu { + pub fn new(channel_id: DynamicChannelId, data: Vec) -> Self { + Self { + header: Header::new(channel_id, 0, Cmd::Data), + channel_id, + data, + } + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub fn into_data(self) -> Vec { + self.data + } + + pub fn data_mut(&mut self) -> &mut Vec { + &mut self.data + } + + fn decode(header: Header, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: header.cb_id.size_of_val()); + let channel_id = header.cb_id.decode_val(src)?; + let data = src.read_remaining().to_vec(); + Ok(Self { + header, + channel_id, + data, + }) + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.header.encode(dst)?; + self.header.cb_id.encode_val(self.channel_id, dst)?; + dst.write_slice(&self.data); + Ok(()) + } + + fn name() -> &'static str { + "DYNVC_DATA" + } + + fn size(&self) -> usize { + strict_sum(&[ + Header::size(), + self.header.cb_id.size_of_val(), // ChannelId + self.data.len(), // Data + ]) + } +} + +/// 2.2.2.2 DVC Create Response PDU (DYNVC_CREATE_RSP) +/// +/// [2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/8f284ea3-54f3-4c24-8168-8a001c63b581 +#[derive(Debug, PartialEq)] +pub struct CreateResponsePdu { + header: Header, + channel_id: DynamicChannelId, + creation_status: CreationStatus, +} + +impl CreateResponsePdu { + pub fn new(channel_id: DynamicChannelId, creation_status: CreationStatus) -> Self { + Self { + header: Header::new(channel_id, 0, Cmd::Create), + channel_id, + creation_status, + } + } + + pub fn channel_id(&self) -> DynamicChannelId { + self.channel_id + } + + pub fn creation_status(&self) -> CreationStatus { + self.creation_status + } + + fn name() -> &'static str { + "DYNVC_CREATE_RSP" + } + + fn decode(header: Header, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::headerless_size(&header)); + let channel_id = header.cb_id.decode_val(src)?; + let creation_status = CreationStatus(src.read_u32()); + Ok(Self { + header, + channel_id, + creation_status, + }) + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.header.encode(dst)?; + self.header.cb_id.encode_val(self.channel_id, dst)?; + self.creation_status.encode(dst)?; + Ok(()) + } + + fn headerless_size(header: &Header) -> usize { + strict_sum(&[ + header.cb_id.size_of_val(), // ChannelId + CreationStatus::size(), // CreationStatus + ]) + } + + fn size(&self) -> usize { + strict_sum(&[Header::size(), Self::headerless_size(&self.header)]) + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct CreationStatus(u32); + +impl CreationStatus { + pub const OK: Self = Self(0x00000000); + pub const NOT_FOUND: Self = Self(0xC0000225); + pub const NO_LISTENER: Self = Self(0xC0000001); + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + dst.write_u32(self.0); + Ok(()) + } + + fn size() -> usize { + 4 + } +} + +impl From for u32 { + fn from(val: CreationStatus) -> Self { + val.0 + } +} + +/// 2.2.4 Closing a DVC (DYNVC_CLOSE) +/// +/// [2.2.4]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/c02dfd21-ccbc-4254-985b-3ef6dd115dec +#[derive(Debug, PartialEq)] +pub struct ClosePdu { + header: Header, + channel_id: DynamicChannelId, +} + +impl ClosePdu { + pub fn new(channel_id: DynamicChannelId) -> Self { + Self { + header: Header::new(channel_id, 0, Cmd::Close), + channel_id, + } + } + + #[must_use] + pub fn with_cb_id_type(self, cb_id: FieldType) -> Self { + Self { + header: self.header.with_cb_id(cb_id), + ..self + } + } + + pub fn channel_id(&self) -> DynamicChannelId { + self.channel_id + } + + fn decode(header: Header, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::headerless_size(&header)); + let channel_id = header.cb_id.decode_val(src)?; + Ok(Self { header, channel_id }) + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.header.encode(dst)?; + self.header.cb_id.encode_val(self.channel_id, dst)?; + Ok(()) + } + + fn name() -> &'static str { + "DYNVC_CLOSE" + } + + fn headerless_size(header: &Header) -> usize { + header.cb_id.size_of_val() + } + + fn size(&self) -> usize { + strict_sum(&[Header::size(), Self::headerless_size(&self.header)]) + } +} + +/// 2.2.1.2 DVC Capabilities Response PDU (DYNVC_CAPS_RSP) +/// +/// [2.2.1.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/d45cb2a6-e7bd-453e-8603-9c57600e24ce +#[derive(Debug, PartialEq)] +pub struct CapabilitiesResponsePdu { + header: Header, + version: CapsVersion, +} + +impl CapabilitiesResponsePdu { + const HEADERLESS_FIXED_PART_SIZE: usize = 1 /* Pad */ + CapsVersion::FIXED_PART_SIZE /* Version */; + const FIXED_PART_SIZE: usize = Header::FIXED_PART_SIZE + Self::HEADERLESS_FIXED_PART_SIZE; + + pub fn new(version: CapsVersion) -> Self { + Self { + header: Header::new(0, 0, Cmd::Capability), + version, + } + } + + fn decode(header: Header, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::HEADERLESS_FIXED_PART_SIZE); + let _pad = src.read_u8(); + let version = CapsVersion::try_from(src.read_u16())?; + Ok(Self { header, version }) + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + self.header.encode(dst)?; + dst.write_u8(0x00); // Pad, MUST be 0x00 + self.version.encode(dst)?; + Ok(()) + } + + fn name() -> &'static str { + "DYNVC_CAPS_RSP" + } + + fn size() -> usize { + Self::FIXED_PART_SIZE + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum CapsVersion { + V1 = 0x0001, + V2 = 0x0002, + V3 = 0x0003, +} + +impl CapsVersion { + const FIXED_PART_SIZE: usize = 2; + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + dst.write_u16(u16::from(*self)); + Ok(()) + } + + fn size() -> usize { + Self::FIXED_PART_SIZE + } +} + +impl TryFrom for CapsVersion { + type Error = DecodeError; + + fn try_from(value: u16) -> Result { + match value { + 0x0001 => Ok(Self::V1), + 0x0002 => Ok(Self::V2), + 0x0003 => Ok(Self::V3), + _ => Err(invalid_field_err!("CapsVersion", "invalid version")), + } + } +} + +impl From for u16 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(version: CapsVersion) -> Self { + version as u16 + } +} + +/// 2.2.1.1 DVC Capabilities Request PDU +/// +/// [2.2.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/c07b15ae-304e-46b8-befe-39c6d95c25e0 +#[derive(Debug, PartialEq)] +pub enum CapabilitiesRequestPdu { + V1 { + header: Header, + }, + V2 { + header: Header, + charges: [u16; CapabilitiesRequestPdu::PRIORITY_CHARGE_COUNT], + }, + V3 { + header: Header, + charges: [u16; CapabilitiesRequestPdu::PRIORITY_CHARGE_COUNT], + }, +} + +impl CapabilitiesRequestPdu { + const HEADERLESS_FIXED_PART_SIZE: usize = 1 /* Pad */ + 2 /* Version */; + const FIXED_PART_SIZE: usize = Header::FIXED_PART_SIZE + Self::HEADERLESS_FIXED_PART_SIZE; + const PRIORITY_CHARGE_SIZE: usize = 2; // 2 bytes for each priority charge + const PRIORITY_CHARGE_COUNT: usize = 4; // 4 priority charges + const PRIORITY_CHARGES_SIZE: usize = Self::PRIORITY_CHARGE_COUNT * Self::PRIORITY_CHARGE_SIZE; + + pub fn new(version: CapsVersion, charges: Option<[u16; Self::PRIORITY_CHARGE_COUNT]>) -> Self { + let header = Header::new(0, 0, Cmd::Capability); + let charges = charges.unwrap_or([0; Self::PRIORITY_CHARGE_COUNT]); + + match version { + CapsVersion::V1 => Self::V1 { header }, + CapsVersion::V2 => Self::V2 { header, charges }, + CapsVersion::V3 => Self::V3 { header, charges }, + } + } + + fn decode(header: Header, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::HEADERLESS_FIXED_PART_SIZE); + let _pad = src.read_u8(); + let version = CapsVersion::try_from(src.read_u16())?; + match version { + CapsVersion::V1 => Ok(Self::V1 { header }), + _ => { + ensure_size!(in: src, size: Self::PRIORITY_CHARGES_SIZE); + let mut charges = [0u16; Self::PRIORITY_CHARGE_COUNT]; + for charge in charges.iter_mut() { + *charge = src.read_u16(); + } + + match version { + CapsVersion::V2 => Ok(Self::V2 { header, charges }), + CapsVersion::V3 => Ok(Self::V3 { header, charges }), + _ => unreachable!(), + } + } + } + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + match self { + CapabilitiesRequestPdu::V1 { header } + | CapabilitiesRequestPdu::V2 { header, .. } + | CapabilitiesRequestPdu::V3 { header, .. } => header.encode(dst)?, + }; + dst.write_u8(0x00); // Pad, MUST be 0x00 + match self { + CapabilitiesRequestPdu::V1 { .. } => dst.write_u16(CapsVersion::V1.into()), + CapabilitiesRequestPdu::V2 { .. } => dst.write_u16(CapsVersion::V2.into()), + CapabilitiesRequestPdu::V3 { .. } => dst.write_u16(CapsVersion::V3.into()), + } + match self { + CapabilitiesRequestPdu::V1 { .. } => {} + CapabilitiesRequestPdu::V2 { charges, .. } | CapabilitiesRequestPdu::V3 { charges, .. } => { + for charge in charges.iter() { + dst.write_u16(*charge); + } + } + } + Ok(()) + } + + fn size(&self) -> usize { + match self { + Self::V1 { .. } => Self::FIXED_PART_SIZE, + _ => Self::FIXED_PART_SIZE + Self::PRIORITY_CHARGES_SIZE, + } + } + + fn name(&self) -> &'static str { + match self { + Self::V1 { .. } => "DYNVC_CAPS_VERSION1", + Self::V2 { .. } => "DYNVC_CAPS_VERSION2", + Self::V3 { .. } => "DYNVC_CAPS_VERSION3", + } + } +} + +/// 2.2.2.1 DVC Create Request PDU (DYNVC_CREATE_REQ) +/// +/// [2.2.2.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/4448ba4d-9a72-429f-8b65-6f4ec44f2985 +#[derive(Debug, PartialEq)] +pub struct CreateRequestPdu { + header: Header, + channel_id: DynamicChannelId, + channel_name: String, +} + +impl CreateRequestPdu { + pub fn new(channel_id: DynamicChannelId, channel_name: String) -> Self { + Self { + header: Header::new(channel_id, 0, Cmd::Create), + channel_id, + channel_name, + } + } + + pub fn channel_id(&self) -> DynamicChannelId { + self.channel_id + } + + pub fn channel_name(&self) -> &str { + &self.channel_name + } + + pub fn into_channel_name(self) -> String { + self.channel_name + } + + fn decode(header: Header, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::headerless_fixed_part_size(&header)); + let channel_id = header.cb_id.decode_val(src)?; + let channel_name = read_string_from_cursor(src, CharacterSet::Ansi, true)?; + Ok(Self { + header, + channel_id, + channel_name, + }) + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.header.encode(dst)?; + self.header.cb_id.encode_val(self.channel_id, dst)?; + write_string_to_cursor(dst, &self.channel_name, CharacterSet::Ansi, true)?; + Ok(()) + } + + fn name() -> &'static str { + "DYNVC_CREATE_REQ" + } + + fn headerless_fixed_part_size(header: &Header) -> usize { + header.cb_id.size_of_val() // ChannelId + } + + fn size(&self) -> usize { + strict_sum(&[ + Header::size(), + Self::headerless_fixed_part_size(&self.header), // ChannelId + encoded_str_len(&self.channel_name, CharacterSet::Ansi, true), // ChannelName + Null terminator + ]) + } +} diff --git a/crates/ironrdp-dvc/src/server.rs b/crates/ironrdp-dvc/src/server.rs new file mode 100644 index 00000000..1811f240 --- /dev/null +++ b/crates/ironrdp-dvc/src/server.rs @@ -0,0 +1,194 @@ +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::fmt; + +use ironrdp_core::{cast_length, impl_as_any, invalid_field_err, Decode as _, DecodeResult, ReadCursor}; +use ironrdp_pdu::{self as pdu, decode_err, encode_err, pdu_other_err}; +use ironrdp_svc::{ChannelFlags, CompressionCondition, SvcMessage, SvcProcessor, SvcServerProcessor}; +use pdu::gcc::ChannelName; +use pdu::PduResult; +use slab::Slab; +use tracing::debug; + +use crate::pdu::{ + CapabilitiesRequestPdu, CapsVersion, CreateRequestPdu, CreationStatus, DrdynvcClientPdu, DrdynvcServerPdu, +}; +use crate::{encode_dvc_messages, CompleteData, DvcProcessor}; + +pub trait DvcServerProcessor: DvcProcessor {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum ChannelState { + Closed, + Creation, + Opened, + CreationFailed(u32), +} + +struct DynamicChannel { + state: ChannelState, + processor: Box, + complete_data: CompleteData, +} + +impl DynamicChannel { + fn new(processor: T) -> Self + where + T: DvcServerProcessor + 'static, + { + Self { + state: ChannelState::Closed, + processor: Box::new(processor), + complete_data: CompleteData::new(), + } + } +} +/// DRDYNVC Static Virtual Channel (the Remote Desktop Protocol: Dynamic Virtual Channel Extension) +/// +/// It adds support for dynamic virtual channels (DVC). +pub struct DrdynvcServer { + dynamic_channels: Slab, +} + +impl fmt::Debug for DrdynvcServer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DrdynvcServer([")?; + + for (i, (id, channel)) in self.dynamic_channels.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}:{} ({:?})", id, channel.processor.channel_name(), channel.state)?; + } + + write!(f, "])") + } +} + +impl DrdynvcServer { + pub const NAME: ChannelName = ChannelName::from_static(b"drdynvc\0"); + + pub fn new() -> Self { + Self { + dynamic_channels: Slab::new(), + } + } + + // FIXME(#61): it’s likely we want to enable adding dynamic channels at any point during the session (message passing? other approach?) + + #[must_use] + pub fn with_dynamic_channel(mut self, channel: T) -> Self + where + T: DvcServerProcessor + 'static, + { + self.dynamic_channels.insert(DynamicChannel::new(channel)); + self + } + + fn channel_by_id(&mut self, id: u32) -> DecodeResult<&mut DynamicChannel> { + let id = cast_length!("DRDYNVC", "", id)?; + self.dynamic_channels + .get_mut(id) + .ok_or_else(|| invalid_field_err!("DRDYNVC", "", "invalid channel id")) + } +} + +impl_as_any!(DrdynvcServer); + +impl Default for DrdynvcServer { + fn default() -> Self { + Self::new() + } +} + +impl SvcProcessor for DrdynvcServer { + fn channel_name(&self) -> ChannelName { + DrdynvcServer::NAME + } + + fn compression_condition(&self) -> CompressionCondition { + CompressionCondition::WhenRdpDataIsCompressed + } + + fn start(&mut self) -> PduResult> { + let cap = CapabilitiesRequestPdu::new(CapsVersion::V1, None); + let req = DrdynvcServerPdu::Capabilities(cap); + let msg = as_svc_msg_with_flag(req)?; + Ok(alloc::vec![msg]) + } + + fn process(&mut self, payload: &[u8]) -> PduResult> { + let pdu = decode_dvc_message(payload).map_err(|e| decode_err!(e))?; + let mut resp = Vec::new(); + + match pdu { + DrdynvcClientPdu::Capabilities(caps_resp) => { + debug!("Got DVC Capabilities Response PDU: {caps_resp:?}"); + for (id, c) in self.dynamic_channels.iter_mut() { + if c.state != ChannelState::Closed { + continue; + } + let req = DrdynvcServerPdu::Create(CreateRequestPdu::new( + id.try_into() + .map_err(|e| pdu_other_err!("invalid channel id", source: e))?, + c.processor.channel_name().into(), + )); + c.state = ChannelState::Creation; + resp.push(as_svc_msg_with_flag(req)?); + } + } + DrdynvcClientPdu::Create(create_resp) => { + debug!("Got DVC Create Response PDU: {create_resp:?}"); + let id = create_resp.channel_id(); + let c = self.channel_by_id(id).map_err(|e| decode_err!(e))?; + if c.state != ChannelState::Creation { + return Err(pdu_other_err!("invalid channel state")); + } + if create_resp.creation_status() != CreationStatus::OK { + c.state = ChannelState::CreationFailed(create_resp.creation_status().into()); + return Ok(resp); + } + c.state = ChannelState::Opened; + let msg = c.processor.start(create_resp.channel_id())?; + resp.extend(encode_dvc_messages(id, msg, ChannelFlags::SHOW_PROTOCOL).map_err(|e| encode_err!(e))?); + } + DrdynvcClientPdu::Close(close_resp) => { + debug!("Got DVC Close Response PDU: {close_resp:?}"); + let c = self + .channel_by_id(close_resp.channel_id()) + .map_err(|e| decode_err!(e))?; + if c.state != ChannelState::Opened { + return Err(pdu_other_err!("invalid channel state")); + } + c.state = ChannelState::Closed; + } + DrdynvcClientPdu::Data(data) => { + let channel_id = data.channel_id(); + let c = self.channel_by_id(channel_id).map_err(|e| decode_err!(e))?; + if c.state != ChannelState::Opened { + debug!(?channel_id, ?c.state, "Invalid channel state"); + return Err(pdu_other_err!("invalid channel state")); + } + if let Some(complete) = c.complete_data.process_data(data).map_err(|e| decode_err!(e))? { + let msg = c.processor.process(channel_id, &complete)?; + resp.extend( + encode_dvc_messages(channel_id, msg, ChannelFlags::SHOW_PROTOCOL) + .map_err(|e| encode_err!(e))?, + ); + } + } + } + + Ok(resp) + } +} + +impl SvcServerProcessor for DrdynvcServer {} + +fn decode_dvc_message(user_data: &[u8]) -> DecodeResult { + DrdynvcClientPdu::decode(&mut ReadCursor::new(user_data)) +} + +fn as_svc_msg_with_flag(pdu: DrdynvcServerPdu) -> PduResult { + Ok(SvcMessage::from(pdu).with_flags(ChannelFlags::SHOW_PROTOCOL)) +} diff --git a/crates/ironrdp-error/CHANGELOG.md b/crates/ironrdp-error/CHANGELOG.md new file mode 100644 index 00000000..87e73d30 --- /dev/null +++ b/crates/ironrdp-error/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-error-v0.1.1...ironrdp-error-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-error-v0.1.0...ironrdp-error-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-error/Cargo.toml b/crates/ironrdp-error/Cargo.toml new file mode 100644 index 00000000..46775fa9 --- /dev/null +++ b/crates/ironrdp-error/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ironrdp-error" +version = "0.1.3" +readme = "README.md" +description = "IronPDU generic error definition" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +std = ["alloc"] +alloc = [] + +[lints] +workspace = true + diff --git a/crates/ironrdp-error/LICENSE-APACHE b/crates/ironrdp-error/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-error/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-error/LICENSE-MIT b/crates/ironrdp-error/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-error/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-error/README.md b/crates/ironrdp-error/README.md new file mode 100644 index 00000000..2732fe67 --- /dev/null +++ b/crates/ironrdp-error/README.md @@ -0,0 +1,7 @@ +# IronRDP Error + +A lightweight and `no_std`-compatible generic `Error` type. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-error/src/lib.rs b/crates/ironrdp-error/src/lib.rs new file mode 100644 index 00000000..82316e74 --- /dev/null +++ b/crates/ironrdp-error/src/lib.rs @@ -0,0 +1,168 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(all(feature = "alloc", not(feature = "std")))] +use alloc::boxed::Box; +use core::fmt; + +#[cfg(feature = "std")] +pub trait Source: core::error::Error + Sync + Send + 'static {} + +#[cfg(feature = "std")] +impl Source for T where T: core::error::Error + Sync + Send + 'static {} + +#[cfg(not(feature = "std"))] +pub trait Source: fmt::Display + fmt::Debug + Send + Sync + 'static {} + +#[cfg(not(feature = "std"))] +impl Source for T where T: fmt::Display + fmt::Debug + Send + Sync + 'static {} + +#[derive(Debug)] +pub struct Error { + context: &'static str, + kind: Kind, + #[cfg(feature = "std")] + source: Option>, + #[cfg(all(not(feature = "std"), feature = "alloc"))] + source: Option>, +} + +impl Error { + #[cold] + #[must_use] + pub fn new(context: &'static str, kind: Kind) -> Self { + Self { + context, + kind, + #[cfg(feature = "alloc")] + source: None, + } + } + + #[cold] + #[must_use] + pub fn with_source(self, source: E) -> Self + where + E: Source, + { + #[cfg(feature = "alloc")] + { + let mut this = self; + this.source = Some(Box::new(source)); + this + } + + // No source when no std and no alloc crates + #[cfg(not(feature = "alloc"))] + { + let _ = source; + self + } + } + + pub fn into_other_kind(self) -> Error + where + Kind: Into, + { + Error { + context: self.context, + kind: self.kind.into(), + #[cfg(any(feature = "std", feature = "alloc"))] + source: self.source, + } + } + + pub fn kind(&self) -> &Kind { + &self.kind + } + + pub fn set_context(&mut self, context: &'static str) { + self.context = context; + } + + pub fn report(&self) -> ErrorReport<'_, Kind> { + ErrorReport(self) + } +} + +impl fmt::Display for Error +where + Kind: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{}] {}", self.context, self.kind) + } +} + +#[cfg(feature = "std")] +impl core::error::Error for Error +where + Kind: core::error::Error, +{ + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + if let Some(source) = self.kind.source() { + Some(source) + } else { + // NOTE: we can’t use Option::as_ref here because of type inference + if let Some(e) = &self.source { + Some(e.as_ref()) + } else { + None + } + } + } +} + +#[cfg(feature = "std")] +impl From> for std::io::Error +where + Kind: core::error::Error + Send + Sync + 'static, +{ + fn from(error: Error) -> Self { + Self::other(error) + } +} + +pub struct ErrorReport<'a, Kind>(&'a Error); + +#[cfg(feature = "std")] +impl fmt::Display for ErrorReport<'_, Kind> +where + Kind: core::error::Error, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use core::error::Error as _; + + write!(f, "{}", self.0)?; + + let mut next_source = self.0.source(); + + while let Some(e) = next_source { + write!(f, ", caused by: {e}")?; + next_source = e.source(); + } + + Ok(()) + } +} + +#[cfg(not(feature = "std"))] +impl fmt::Display for ErrorReport<'_, E> +where + E: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0)?; + + #[cfg(feature = "alloc")] + if let Some(source) = &self.0.source { + write!(f, ", caused by: {source}")?; + } + + Ok(()) + } +} diff --git a/crates/ironrdp-futures/CHANGELOG.md b/crates/ironrdp-futures/CHANGELOG.md new file mode 100644 index 00000000..3a35de72 --- /dev/null +++ b/crates/ironrdp-futures/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-futures-v0.5.0...ironrdp-futures-v0.6.0)] - 2025-12-18 + + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-futures-v0.1.2...ironrdp-futures-v0.1.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-futures-v0.1.1...ironrdp-futures-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + diff --git a/crates/ironrdp-futures/Cargo.toml b/crates/ironrdp-futures/Cargo.toml new file mode 100644 index 00000000..ed75e138 --- /dev/null +++ b/crates/ironrdp-futures/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ironrdp-futures" +version = "0.6.0" +readme = "README.md" +description = "`Framed*` traits implementation above futures’s traits" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +futures-util = { version = "0.3", features = ["io"] } # public +ironrdp-async = { path = "../ironrdp-async", version = "0.8" } # public +bytes = "1" # public + +[lints] +workspace = true + diff --git a/crates/ironrdp-futures/LICENSE-APACHE b/crates/ironrdp-futures/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-futures/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-futures/LICENSE-MIT b/crates/ironrdp-futures/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-futures/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-futures/README.md b/crates/ironrdp-futures/README.md new file mode 100644 index 00000000..437dfd3b --- /dev/null +++ b/crates/ironrdp-futures/README.md @@ -0,0 +1,7 @@ +# IronRDP Futures + +`Framed*` traits implementation above `futures`’s traits. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-futures/src/lib.rs b/crates/ironrdp-futures/src/lib.rs new file mode 100644 index 00000000..b9b76da4 --- /dev/null +++ b/crates/ironrdp-futures/src/lib.rs @@ -0,0 +1,151 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +#[rustfmt::skip] // do not re-order this pub use +pub use ironrdp_async::*; + +use core::pin::Pin; +use std::io; + +use bytes::BytesMut; +use futures_util::io::{AsyncRead, AsyncWrite}; + +pub type FuturesFramed = Framed>; + +pub struct FuturesStream { + inner: S, +} + +impl StreamWrapper for FuturesStream { + type InnerStream = S; + + fn from_inner(stream: Self::InnerStream) -> Self { + Self { inner: stream } + } + + fn into_inner(self) -> Self::InnerStream { + self.inner + } + + fn get_inner(&self) -> &Self::InnerStream { + &self.inner + } + + fn get_inner_mut(&mut self) -> &mut Self::InnerStream { + &mut self.inner + } +} + +impl FramedRead for FuturesStream +where + S: Send + Sync + Unpin + AsyncRead, +{ + type ReadFut<'read> + = Pin> + Send + Sync + 'read>> + where + Self: 'read; + + fn read<'a>(&'a mut self, buf: &'a mut BytesMut) -> Self::ReadFut<'a> { + use futures_util::io::AsyncReadExt as _; + + Box::pin(async { + // NOTE(perf): tokio implementation is more efficient + let mut read_bytes = [0u8; 1024]; + let len = self.inner.read(&mut read_bytes).await?; + buf.extend_from_slice(&read_bytes[..len]); + + Ok(len) + }) + } +} + +impl FramedWrite for FuturesStream +where + S: Send + Sync + Unpin + AsyncWrite, +{ + type WriteAllFut<'write> + = Pin> + Send + Sync + 'write>> + where + Self: 'write; + + fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a> { + use futures_util::io::AsyncWriteExt as _; + + Box::pin(async { + self.inner.write_all(buf).await?; + self.inner.flush().await?; + + Ok(()) + }) + } +} + +pub type LocalFuturesFramed = Framed>; + +pub struct LocalFuturesStream { + inner: S, +} + +impl StreamWrapper for LocalFuturesStream { + type InnerStream = S; + + fn from_inner(stream: Self::InnerStream) -> Self { + Self { inner: stream } + } + + fn into_inner(self) -> Self::InnerStream { + self.inner + } + + fn get_inner(&self) -> &Self::InnerStream { + &self.inner + } + + fn get_inner_mut(&mut self) -> &mut Self::InnerStream { + &mut self.inner + } +} + +impl FramedRead for LocalFuturesStream +where + S: Unpin + AsyncRead, +{ + type ReadFut<'read> + = Pin> + 'read>> + where + Self: 'read; + + fn read<'a>(&'a mut self, buf: &'a mut BytesMut) -> Self::ReadFut<'a> { + use futures_util::io::AsyncReadExt as _; + + Box::pin(async { + // NOTE(perf): tokio implementation is more efficient + let mut read_bytes = [0u8; 1024]; + let len = self.inner.read(&mut read_bytes[..]).await?; + buf.extend_from_slice(&read_bytes[..len]); + + Ok(len) + }) + } +} + +impl FramedWrite for LocalFuturesStream +where + S: Unpin + AsyncWrite, +{ + type WriteAllFut<'write> + = Pin> + 'write>> + where + Self: 'write; + + fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a> { + use futures_util::io::AsyncWriteExt as _; + + Box::pin(async { + self.inner.write_all(buf).await?; + self.inner.flush().await?; + + Ok(()) + }) + } +} diff --git a/crates/ironrdp-fuzzing/Cargo.toml b/crates/ironrdp-fuzzing/Cargo.toml new file mode 100644 index 00000000..4d482f6d --- /dev/null +++ b/crates/ironrdp-fuzzing/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ironrdp-fuzzing" +version = "0.0.0" +edition = "2021" +description = "Provides test case generators and oracles for use with IronRDP fuzzing" +publish = false + +[lib] +doctest = false +test = false + +[dependencies] +arbitrary = { version = "1", features = ["derive"] } +ironrdp-core.path = "../ironrdp-core" +ironrdp-graphics.path = "../ironrdp-graphics" +ironrdp-pdu.path = "../ironrdp-pdu" +ironrdp-cliprdr.path = "../ironrdp-cliprdr" +ironrdp-rdpdr.path = "../ironrdp-rdpdr" +ironrdp-rdpsnd.path = "../ironrdp-rdpsnd" +ironrdp-cliprdr-format.path = "../ironrdp-cliprdr-format" +ironrdp-displaycontrol.path = "../ironrdp-displaycontrol" +ironrdp-svc.path = "../ironrdp-svc" + +[lints] +workspace = true + diff --git a/crates/ironrdp-fuzzing/src/generators/mod.rs b/crates/ironrdp-fuzzing/src/generators/mod.rs new file mode 100644 index 00000000..ed5232cd --- /dev/null +++ b/crates/ironrdp-fuzzing/src/generators/mod.rs @@ -0,0 +1,16 @@ +//! Test case generators. +//! +//! Test case generators take raw, unstructured input from a fuzzer +//! (e.g. libFuzzer) and translate that into a structured test case (e.g. a +//! valid RDP PDU). +//! +//! These are generally implementations of the `Arbitrary` trait, or some +//! wrapper over an external tool, such that the wrapper implements the +//! `Arbitrary` trait for the wrapped external tool. + +#[derive(arbitrary::Arbitrary, Debug)] +pub struct BitmapInput<'a> { + pub src: &'a [u8], + pub width: u8, + pub height: u8, +} diff --git a/crates/ironrdp-fuzzing/src/lib.rs b/crates/ironrdp-fuzzing/src/lib.rs new file mode 100644 index 00000000..c92aa7cf --- /dev/null +++ b/crates/ironrdp-fuzzing/src/lib.rs @@ -0,0 +1,9 @@ +// No need to be as strict as in production libraries +#![allow(clippy::arithmetic_side_effects)] +#![allow(clippy::cast_lossless)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_possible_wrap)] +#![allow(clippy::cast_sign_loss)] + +pub mod generators; +pub mod oracles; diff --git a/crates/ironrdp-fuzzing/src/oracles/mod.rs b/crates/ironrdp-fuzzing/src/oracles/mod.rs new file mode 100644 index 00000000..2858316c --- /dev/null +++ b/crates/ironrdp-fuzzing/src/oracles/mod.rs @@ -0,0 +1,147 @@ +//! Oracles. +//! +//! Oracles take a test case and determine whether we have a bug. For example, +//! one of the simplest oracles is to take a RDP PDU as our input test case, +//! encode and decode it, and (implicitly) check that no assertions +//! failed or segfaults happened. A more complicated oracle might compare the +//! result of two different implementations for the same thing, and +//! make sure that the two executions are observably identical (differential fuzzing). +//! +//! When an oracle finds a bug, it should report it to the fuzzing engine by +//! panicking. + +use crate::generators::BitmapInput; + +pub fn pdu_decode(data: &[u8]) { + use ironrdp_core::decode; + use ironrdp_pdu::mcs::{ConnectInitial, ConnectResponse, McsMessage}; + use ironrdp_pdu::nego::{ConnectionConfirm, ConnectionRequest}; + use ironrdp_pdu::rdp::{capability_sets, headers, server_error_info, server_license, vc, ClientInfoPdu}; + use ironrdp_pdu::x224::X224; + use ironrdp_pdu::{bitmap, codecs, fast_path, gcc, input, pcb, surface_commands}; + + let _ = decode::>(data); + let _ = decode::>(data); + let _ = decode::>>(data); + let _ = decode::(data); + let _ = decode::(data); + let _ = decode::(data); + let _ = decode::(data); + let _ = decode::(data); + let _ = decode::(data); + let _ = decode::(data); + + let _ = decode::(data); + let _ = decode::(data); + let _ = decode::(data); + let _ = decode::(data); + let _ = decode::(data); + + let _ = decode::(data); + + let _ = decode::(data); + + let _ = decode::(data); + let _ = decode::>(data); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::Orders); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::Bitmap); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::Palette); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::Synchronize); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::SurfaceCommands); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::HiddenPointer); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::DefaultPointer); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::PositionPointer); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::ColorPointer); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::CachedPointer); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::NewPointer); + let _ = fast_path::FastPathUpdate::decode_with_code(data, fast_path::UpdateCode::LargePointer); + + let _ = decode::>(data); + let _ = decode::>(data); + let _ = decode::(data); + let _ = decode::>(data); + let _ = decode::(data); + + let _ = decode::>(data); + + let _ = decode::(data); + let _ = decode::(data); + + let _ = decode::>(data); + + let _ = decode::>(data); + + let _ = decode::(data); + + let _ = decode::(data); + + let _ = decode::>(data); + let _ = decode::(data); +} + +pub fn rle_decompress_bitmap(input: BitmapInput<'_>) { + let mut out = Vec::new(); + + let _ = ironrdp_graphics::rle::decompress_24_bpp(input.src, &mut out, input.width.into(), input.height.into()); + let _ = ironrdp_graphics::rle::decompress_16_bpp(input.src, &mut out, input.width.into(), input.height.into()); + let _ = ironrdp_graphics::rle::decompress_15_bpp(input.src, &mut out, input.width.into(), input.height.into()); + let _ = ironrdp_graphics::rle::decompress_8_bpp(input.src, &mut out, input.width.into(), input.height.into()); +} + +pub fn rdp6_encode_bitmap_stream(input: &BitmapInput<'_>) { + use ironrdp_graphics::rdp6::{BitmapStreamEncoder, RgbAChannels, RgbChannels}; + + let mut out = vec![0; input.src.len() * 2]; + + let _ = BitmapStreamEncoder::new(input.width.into(), input.height.into()).encode_bitmap::( + input.src, + out.as_mut_slice(), + false, + ); + + let _ = BitmapStreamEncoder::new(input.width.into(), input.height.into()).encode_bitmap::( + input.src, + out.as_mut_slice(), + true, + ); +} + +pub fn rdp6_decode_bitmap_stream_to_rgb24(input: &BitmapInput<'_>) { + use ironrdp_graphics::rdp6::BitmapStreamDecoder; + + let mut out = Vec::new(); + + let _ = BitmapStreamDecoder::default().decode_bitmap_stream_to_rgb24( + input.src, + &mut out, + usize::from(input.width), + usize::from(input.height), + ); +} + +pub fn cliprdr_format(input: &[u8]) { + use ironrdp_cliprdr_format::bitmap::{dib_to_png, dibv5_to_png, png_to_cf_dib, png_to_cf_dibv5}; + use ironrdp_cliprdr_format::html::{cf_html_to_plain_html, plain_html_to_cf_html}; + + let _ = png_to_cf_dib(input); + let _ = png_to_cf_dibv5(input); + + let _ = dib_to_png(input); + let _ = dibv5_to_png(input); + + let _ = cf_html_to_plain_html(input); + + if let Ok(input) = core::str::from_utf8(input) { + let _ = plain_html_to_cf_html(input); + } +} + +pub fn channel_process(input: &[u8]) { + use ironrdp_svc::SvcProcessor as _; + + let mut rdpdr = ironrdp_rdpdr::Rdpdr::new(Box::new(ironrdp_rdpdr::NoopRdpdrBackend), "Backend".to_owned()) + .with_smartcard(1) + .with_drives(None); + + let _ = rdpdr.process(input); +} diff --git a/crates/ironrdp-glutin-renderer/Cargo.toml b/crates/ironrdp-glutin-renderer/Cargo.toml new file mode 100644 index 00000000..d418a342 --- /dev/null +++ b/crates/ironrdp-glutin-renderer/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ironrdp-glutin-renderer" +version = "0.1.0" +readme = "README.md" +description = "`glutin` primitives for OpenGL rendering" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +ironrdp.workspace = true +tracing.workspace = true +thiserror.workspace = true +glow = "0.12" +glutin = { version = "0.29" } +openh264 = { version = "0.4" } + +[lints] +workspace = true + diff --git a/crates/ironrdp-glutin-renderer/shaders/avc.vert b/crates/ironrdp-glutin-renderer/shaders/avc.vert new file mode 100644 index 00000000..2477588c --- /dev/null +++ b/crates/ironrdp-glutin-renderer/shaders/avc.vert @@ -0,0 +1,8 @@ +precision mediump float; + +uniform mat4 u_projection; +attribute vec2 a_position; + +void main(){ + gl_Position = u_projection * vec4(a_position, 0.0, 1.0); +} \ No newline at end of file diff --git a/crates/ironrdp-glutin-renderer/shaders/avc420.frag b/crates/ironrdp-glutin-renderer/shaders/avc420.frag new file mode 100644 index 00000000..639873c2 --- /dev/null +++ b/crates/ironrdp-glutin-renderer/shaders/avc420.frag @@ -0,0 +1,35 @@ +precision lowp float; + +uniform vec2 screen_size; +uniform vec2 stride_scale; +uniform sampler2D main_y_texture; +uniform sampler2D main_u_texture; +uniform sampler2D main_v_texture; + +uniform sampler2D aux_y_texture; +uniform sampler2D aux_u_texture; +uniform sampler2D aux_v_texture; + +// YUV to RGB conversion matrix from https://github.com/mbebenita/Broadway/blob/master/Player/YUVCanvas.js +const mat4 conversion = mat4( + 1.16438, 0.00000, 1.79274, -0.97295, + 1.16438, -0.21325, -0.53291, 0.30148, + 1.16438, 2.11240, 0.00000, -1.13340, + 0, 0, 0, 1 +); + +void main(void) { + // Inverted image + vec2 coordinates = vec2(gl_FragCoord.x, screen_size.y - gl_FragCoord.y); + + // Scale from [0..width, 0..height] to [0..1.0, 0..1.0] range and + // then scale to eliminate the stride padding + vec2 tex_coord = ((coordinates) / screen_size) * stride_scale; + + float main_y_channel = texture2D(main_y_texture, tex_coord).x; + float main_u_channel = texture2D(main_u_texture, tex_coord).x; + float main_v_channel = texture2D(main_v_texture, tex_coord).x; + + vec4 channels = vec4(main_y_channel, main_u_channel, main_v_channel, 1.0); + gl_FragColor = channels * conversion; +} \ No newline at end of file diff --git a/crates/ironrdp-glutin-renderer/shaders/avc444.frag b/crates/ironrdp-glutin-renderer/shaders/avc444.frag new file mode 100644 index 00000000..9851b8b2 --- /dev/null +++ b/crates/ironrdp-glutin-renderer/shaders/avc444.frag @@ -0,0 +1,77 @@ +precision lowp float; + +uniform vec2 screen_size; +uniform vec2 stride_scale; +uniform sampler2D main_y_texture; +uniform sampler2D main_u_texture; +uniform sampler2D main_v_texture; + +uniform sampler2D aux_y_texture; +uniform sampler2D aux_u_texture; +uniform sampler2D aux_v_texture; + +const vec2 CUTOFF = vec2(30.0/255.0, 30.0/255.0); + +// YUV to RGB conversion matrix from https://github.com/mbebenita/Broadway/blob/master/Player/YUVCanvas.js +const mat4 conversion = mat4( + 1.16438, 0.00000, 1.79274, -0.97295, + 1.16438, -0.21325, -0.53291, 0.30148, + 1.16438, 2.11240, 0.00000, -1.13340, + 0, 0, 0, 1 +); + +const vec2 half_offset = vec2(0.5, 0.5); + +void main(void) { + vec2 coordinates = vec2(gl_FragCoord.x, screen_size.y - gl_FragCoord.y) ; + vec2 main_tex_coord = (coordinates / screen_size) * stride_scale; + // Query the main view + float main_y_channel = texture2D(main_y_texture, main_tex_coord).x; + float main_u_channel = texture2D(main_u_texture, main_tex_coord).x; + float main_v_channel = texture2D(main_v_texture, main_tex_coord).x; + + coordinates = coordinates - half_offset; + + float offset = floor(mod(coordinates.y, 16.0) * 0.5); + float start_y = offset + floor(coordinates.y / 16.0) * 16.0; + // Auxiliary view + vec2 aux_tex_coord = vec2(coordinates.x, start_y) + half_offset; + vec2 aux_tex_coord_next = aux_tex_coord + vec2(1.0, 0.0); + + vec2 top_half = aux_tex_coord/screen_size * stride_scale; + vec2 top_half_next = aux_tex_coord_next/screen_size * stride_scale; + vec2 bottom_half = (aux_tex_coord + vec2(0.0, 8.0))/screen_size * stride_scale; + vec2 bottom_half_next = (aux_tex_coord_next + vec2(0.0, 8.0))/screen_size * stride_scale; + + float aux_b4 = texture2D(aux_y_texture, top_half).x; + float aux_b5 = texture2D(aux_y_texture, bottom_half).x; + float next_u = texture2D(aux_y_texture, top_half_next).x; + float next_v = texture2D(aux_y_texture, bottom_half_next).x; + vec2 aux_uv_additional = vec2(next_u, next_v); + + float aux_b6 = texture2D(aux_u_texture, main_tex_coord).x; + float aux_b7 = texture2D(aux_v_texture, main_tex_coord).x; + + float is_x_odd = mod(coordinates.x, 2.0); + float is_y_odd = mod(coordinates.y, 2.0); + float is_xy_even = (1.0 - is_x_odd) * (1.0 - is_y_odd); + + vec2 aux_uv_main = vec2(aux_b4, aux_b5); + vec2 aux_uv_secondary = vec2(aux_b6, aux_b7); + + vec2 uv_channels = is_y_odd * aux_uv_main + (1.0 - is_y_odd) * is_x_odd * aux_uv_secondary; + + // Apply the reverse filter when both (x, y) are even based on [MS-RDPEGFX] rule + vec2 main_uv=vec2(main_u_channel, main_v_channel); + vec2 uv_augmented = clamp(main_uv * 4.0 - aux_uv_main - aux_uv_secondary - aux_uv_additional, vec2(0.0, 0.0), vec2(1.0, 1.0)); + vec2 uv_diff = abs(uv_augmented - main_uv); + bvec2 uv_is_greater = greaterThan(uv_diff, CUTOFF); + vec2 uv_is_greater_vec = vec2(uv_is_greater.x, uv_is_greater.y); + vec2 uv_touse = uv_augmented * uv_is_greater_vec + main_uv * (vec2(1.0, 1.0) - uv_is_greater_vec); + + vec2 final_uv_channels = is_xy_even * uv_touse + (1.0 - is_xy_even) * uv_channels; + + vec4 channels = vec4(main_y_channel, final_uv_channels.x, final_uv_channels.y, 1.0); + vec3 rgb = (channels * conversion).xyz; + gl_FragColor = vec4(rgb, 1.0); +} \ No newline at end of file diff --git a/crates/ironrdp-glutin-renderer/shaders/avc444v2.frag b/crates/ironrdp-glutin-renderer/shaders/avc444v2.frag new file mode 100644 index 00000000..872e206d --- /dev/null +++ b/crates/ironrdp-glutin-renderer/shaders/avc444v2.frag @@ -0,0 +1,79 @@ +precision lowp float; + +uniform vec2 screen_size; +uniform vec2 stride_scale; +uniform sampler2D main_y_texture; +uniform sampler2D main_u_texture; +uniform sampler2D main_v_texture; + +uniform sampler2D aux_y_texture; +uniform sampler2D aux_u_texture; +uniform sampler2D aux_v_texture; + +const vec2 CUTOFF = vec2(30.0/255.0, 30.0/255.0); + +// YUV to RGB conversion matrix from https://github.com/mbebenita/Broadway/blob/master/Player/YUVCanvas.js +const mat4 conversion = mat4( + 1.16438, 0.00000, 1.79274, -0.97295, + 1.16438, -0.21325, -0.53291, 0.30148, + 1.16438, 2.11240, 0.00000, -1.13340, + 0, 0, 0, 1 +); + +const vec2 half_offset = vec2(0.5, 0.5); + +void main(void) { + + vec2 coordinates = vec2(gl_FragCoord.x, screen_size.y - gl_FragCoord.y); + + // Query the main view + vec2 main_tex_coord = (coordinates / screen_size) * stride_scale; + float main_y_channel = texture2D(main_y_texture, main_tex_coord).x; + float main_u_channel = texture2D(main_u_texture, main_tex_coord).x; + float main_v_channel = texture2D(main_v_texture, main_tex_coord).x; + + coordinates = coordinates - half_offset; + float left_x = coordinates.x * 0.5; + float right_x = (screen_size.x + coordinates.x) * 0.5; + + // Auxiliary view + // Left + vec2 left_half = (vec2(left_x, coordinates.y) + half_offset)/screen_size * stride_scale; + float aux_ub4 = texture2D(aux_y_texture, left_half).x; + float aux_ub6 = texture2D(aux_u_texture, left_half).x; + float aux_ub8 = texture2D(aux_v_texture, left_half).x; + + // Right + vec2 right_half = (vec2(right_x, coordinates.y) + half_offset)/screen_size * stride_scale; + float aux_vb5 = texture2D(aux_y_texture, right_half).x; + float aux_vb7 = texture2D(aux_u_texture, right_half).x; + float aux_vb9 = texture2D(aux_v_texture, right_half).x; + + // Create aux view + vec2 aux_uv_main = vec2(aux_ub4, aux_vb5); + vec2 aux_uv_left = vec2(aux_ub6, aux_vb7); + vec2 aux_uv_right = vec2(aux_ub8, aux_vb9); + + float is_x_odd = mod(coordinates.x, 2.0); + float is_y_odd = mod(coordinates.y, 2.0); + float is_xy_even = (1.0 - is_x_odd) * (1.0 - is_y_odd); + float is_x_mod_4 = float(mod(coordinates.x, 4.0) < 1.0); + + // If x is odd then b4,b5 have data + // else if y is odd then b6,b7 have data when x is divisible by 4 + // else b8,b9 have data + vec2 uv_channels = is_x_odd * aux_uv_main + (1.0 - is_x_odd) * is_y_odd * (is_x_mod_4 * aux_uv_left + (1.0 - is_x_mod_4) * aux_uv_right); + + // Apply the reverse filter when both (x, y) are even based on [MS-RDPEGFX] rule + vec2 main_uv=vec2(main_u_channel, main_v_channel); + vec2 uv_augmented = clamp(main_uv * 4.0 - aux_uv_main - aux_uv_left - aux_uv_right, vec2(0.0, 0.0), vec2(1.0, 1.0)); + vec2 uv_diff = abs(uv_augmented - main_uv); + bvec2 uv_is_greater = greaterThan(uv_diff, CUTOFF); + vec2 uv_is_greater_vec = vec2(uv_is_greater.x, uv_is_greater.y); + vec2 uv_touse = uv_augmented * uv_is_greater_vec + main_uv * (vec2(1.0, 1.0) - uv_is_greater_vec); + + vec2 final_uv_channels = is_xy_even * uv_touse + (1.0 - is_xy_even) * uv_channels; + vec4 channels = vec4(main_y_channel, final_uv_channels.x, final_uv_channels.y, 1.0); + vec3 rgb = (channels * conversion).xyz; + gl_FragColor = vec4(rgb, 1.0); +} \ No newline at end of file diff --git a/crates/ironrdp-glutin-renderer/shaders/texture_shader.frag b/crates/ironrdp-glutin-renderer/shaders/texture_shader.frag new file mode 100644 index 00000000..bf62522f --- /dev/null +++ b/crates/ironrdp-glutin-renderer/shaders/texture_shader.frag @@ -0,0 +1,9 @@ +precision lowp float; + +varying vec2 v_texCoord; +uniform sampler2D screen_texture; + +void main(void) { + vec4 color = texture2D(screen_texture, v_texCoord); + gl_FragColor = color; +} \ No newline at end of file diff --git a/crates/ironrdp-glutin-renderer/shaders/texture_shader.vert b/crates/ironrdp-glutin-renderer/shaders/texture_shader.vert new file mode 100644 index 00000000..cfcf6a18 --- /dev/null +++ b/crates/ironrdp-glutin-renderer/shaders/texture_shader.vert @@ -0,0 +1,10 @@ +precision mediump float; + +attribute vec2 a_position; +attribute vec2 a_tex_coord; +varying vec2 v_texCoord; + +void main(){ + v_texCoord = a_tex_coord; + gl_Position = vec4(a_position, 0.0, 1.0); +} \ No newline at end of file diff --git a/crates/ironrdp-glutin-renderer/src/draw.rs b/crates/ironrdp-glutin-renderer/src/draw.rs new file mode 100644 index 00000000..40858963 --- /dev/null +++ b/crates/ironrdp-glutin-renderer/src/draw.rs @@ -0,0 +1,604 @@ +use std::iter::FromIterator; +use std::mem::size_of; +use std::slice::from_raw_parts; +use std::sync::Arc; + +use glow::*; +use ironrdp::pdu::geometry::{InclusiveRectangle, Rectangle as _}; + +fn cast_as_bytes(input: &[T]) -> &[u8] { + unsafe { from_raw_parts(input.as_ptr() as *const u8, input.len() * size_of::()) } +} + +pub struct DrawingTexture { + gl: Arc, + texture: Texture, + location: UniformLocation, + height: i32, +} + +impl DrawingTexture { + unsafe fn new(gl_ref: Arc, program: Program, location: &str, height: i32) -> Self { + let gl = &gl_ref; + let location = gl.get_uniform_location(program, location).unwrap(); + let texture = gl.create_texture().unwrap(); + gl.bind_texture(TEXTURE_2D, Some(texture)); + gl.tex_parameter_i32(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST as i32); + gl.tex_parameter_i32(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST as i32); + gl.tex_parameter_i32(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE as i32); + gl.tex_parameter_i32(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE as i32); + gl.bind_texture(TEXTURE_2D, None); + DrawingTexture { + gl: gl_ref.clone(), + texture, + location, + height, + } + } + + /// # Safety + /// + /// TODO: Safety notes + pub unsafe fn bind_texture(&self, gl: &Context, pixels: &[u8], stride: i32) { + gl.bind_texture(TEXTURE_2D, Some(self.texture)); + gl.tex_image_2d( + TEXTURE_2D, + 0, + glow::R8 as i32, + stride, + self.height, + 0, + glow::RED, + UNSIGNED_BYTE, + Some(pixels), + ); + gl.bind_texture(TEXTURE_2D, None); + } +} + +impl Drop for DrawingTexture { + fn drop(&mut self) { + unsafe { + self.gl.delete_texture(self.texture); + } + } +} + +pub struct DrawingTextures { + y: DrawingTexture, + u: DrawingTexture, + v: DrawingTexture, + height: i32, + index: i32, +} + +impl DrawingTextures { + /// # Safety + /// + /// TODO: Safety notes + pub unsafe fn new( + gl_ref: Arc, + program: Program, + height: i32, + y_location: &str, + u_location: &str, + v_location: &str, + index: i32, + ) -> Self { + let y = DrawingTexture::new(gl_ref.clone(), program, y_location, height); + let u = DrawingTexture::new(gl_ref.clone(), program, u_location, height / 2); + let v = DrawingTexture::new(gl_ref, program, v_location, height / 2); + + DrawingTextures { y, u, v, height, index } + } + + /// # Safety + /// + /// TODO: Safety notes + pub unsafe fn bind(&self, gl: &Context, data: &[u8], stride_0: usize, stride_1: usize) { + let luma = stride_0 * self.height as usize; + let chroma = stride_1 * (self.height as usize / 2); + let y_pixels = &data[0..luma]; + let u_pixels = &data[luma..luma + chroma]; + let v_pixels = &data[luma + chroma..]; + + self.y.bind_texture(gl, y_pixels, stride_0 as i32); + self.u.bind_texture(gl, u_pixels, stride_1 as i32); + self.v.bind_texture(gl, v_pixels, stride_1 as i32); + + self.activate(gl); + } + + unsafe fn activate(&self, gl: &Context) { + gl.uniform_1_i32(Some(&self.y.location), self.index); + gl.active_texture(TEXTURE0 + self.index as u32); + gl.bind_texture(TEXTURE_2D, Some(self.y.texture)); + + gl.uniform_1_i32(Some(&self.u.location), self.index + 1); + gl.active_texture(TEXTURE0 + 1 + self.index as u32); + gl.bind_texture(TEXTURE_2D, Some(self.u.texture)); + + gl.uniform_1_i32(Some(&self.v.location), self.index + 2); + gl.active_texture(TEXTURE0 + 2 + self.index as u32); + gl.bind_texture(TEXTURE_2D, Some(self.v.texture)); + gl.active_texture(TEXTURE0 + 3 + self.index as u32); + } +} + +#[derive(Debug, Copy, Clone)] +pub enum ShaderType { + Avc420, + Avc444, + Avc444v2, + TextureShader, +} + +impl ShaderType { + fn get_fragment_shader_source(&self) -> String { + let mut source = String::new(); + match self { + ShaderType::Avc444 => { + source.push_str(include_str!("../shaders/avc444.frag")); + } + ShaderType::Avc444v2 => { + source.push_str(include_str!("../shaders/avc444v2.frag")); + } + ShaderType::Avc420 => { + source.push_str(include_str!("../shaders/avc420.frag")); + } + ShaderType::TextureShader => { + source.push_str(include_str!("../shaders/texture_shader.frag")); + } + } + source + } + + fn get_vertex_shader_source(&self) -> String { + let mut source = String::new(); + match self { + ShaderType::Avc420 | ShaderType::Avc444 | ShaderType::Avc444v2 => { + source.push_str(include_str!("../shaders/avc.vert")); + } + ShaderType::TextureShader => { + source.push_str(include_str!("../shaders/texture_shader.vert")); + } + } + source + } + + unsafe fn create_shader(&self, gl: &Context, shader_version: &str) -> crate::Result { + let vertex_shader_source = self.get_vertex_shader_source(); + let fragment_shader_source = self.get_fragment_shader_source(); + + let shader_sources = [ + (glow::VERTEX_SHADER, vertex_shader_source), + (glow::FRAGMENT_SHADER, fragment_shader_source), + ]; + + let program = gl.create_program()?; + let mut shaders = Vec::with_capacity(shader_sources.len()); + + for (shader_type, shader_source) in shader_sources.iter() { + let shader = gl.create_shader(*shader_type)?; + gl.shader_source(shader, &format!("{shader_version}\n{shader_source}")); + gl.compile_shader(shader); + if !gl.get_shader_compile_status(shader) { + return Err(crate::Error::from(gl.get_shader_info_log(shader))); + } + gl.attach_shader(program, shader); + shaders.push(shader); + } + + gl.link_program(program); + if !gl.get_program_link_status(program) { + return Err(crate::Error::from(gl.get_program_info_log(program))); + } + + for shader in shaders { + gl.detach_shader(program, shader); + gl.delete_shader(shader); + } + + Ok(program) + } +} + +pub struct OffscreenBuffer { + gl: Arc, + texture: Texture, + frame_buffer: Framebuffer, +} + +impl Drop for OffscreenBuffer { + fn drop(&mut self) { + unsafe { + self.gl.delete_texture(self.texture); + } + } +} + +impl OffscreenBuffer { + unsafe fn new(gl_ref: Arc, width: i32, height: i32) -> crate::Result { + let gl = &gl_ref; + let texture = gl.create_texture()?; + gl.bind_texture(TEXTURE_2D, Some(texture)); + gl.tex_image_2d(TEXTURE_2D, 0, RGB as i32, width, height, 0, RGB, UNSIGNED_BYTE, None); + + let frame_buffer = gl.create_framebuffer()?; + gl.bind_framebuffer(FRAMEBUFFER, Some(frame_buffer)); + gl.framebuffer_texture_2d(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, Some(texture), 0); + gl.bind_framebuffer(FRAMEBUFFER, None); + Ok(OffscreenBuffer { + gl: gl_ref.clone(), + texture, + frame_buffer, + }) + } + + unsafe fn activate(&self) { + self.gl.bind_framebuffer(FRAMEBUFFER, Some(self.frame_buffer)); + } + + unsafe fn deactivate(&self) { + self.gl.bind_framebuffer(FRAMEBUFFER, None); + } +} + +pub struct TextureShaderProgram { + gl: Arc, + program: Program, + screen_texture_location: UniformLocation, + vertex_buffer: Buffer, + vertex_array: VertexArray, +} + +impl TextureShaderProgram { + unsafe fn new( + gl_ref: Arc, + shader_version: &str, + width: i32, + height: i32, + texture_width: i32, + texture_height: i32, + ) -> crate::Result { + let gl = &gl_ref; + let program = ShaderType::TextureShader.create_shader(gl, shader_version)?; + gl.use_program(Some(program)); + + let a_position = gl.get_attrib_location(program, "a_position").unwrap(); + let a_tex_coord = gl.get_attrib_location(program, "a_tex_coord").unwrap(); + let screen_texture_location = gl.get_uniform_location(program, "screen_texture").unwrap(); + + // If video height is higher trim the padding on the bottom + let y_location = if texture_height > height { + (texture_height - height) as f32 / height as f32 + } else { + 0.0 + }; + + // If video width is higher trim the padding on the right + let x_location = if texture_width > width { + 1.0 - (texture_width - width) as f32 / width as f32 + } else { + 1.0 + }; + + #[rustfmt::skip] + let data : Vec = vec![ + -1.0, -1.0, 0.0, y_location, + 1.0, -1.0, x_location, y_location, + -1.0, 1.0, 0.0, 1.0, + -1.0, 1.0, 0.0, 1.0, + 1.0, -1.0, x_location, y_location, + 1.0, 1.0, x_location, 1.0, + ]; + + let vertex_array = gl.create_vertex_array()?; + let vertex_buffer = gl.create_buffer().unwrap(); + gl.bind_vertex_array(Some(vertex_array)); + gl.bind_buffer(ARRAY_BUFFER, Some(vertex_buffer)); + + gl.enable_vertex_attrib_array(a_tex_coord); + gl.enable_vertex_attrib_array(a_position); + + gl.buffer_data_u8_slice(ARRAY_BUFFER, cast_as_bytes(data.as_ref()), DYNAMIC_DRAW); + gl.vertex_attrib_pointer_f32(a_position, 2, FLOAT, false, 16, 0); + gl.vertex_attrib_pointer_f32(a_tex_coord, 2, FLOAT, false, 16, 8); + + Ok(TextureShaderProgram { + gl: gl_ref.clone(), + program, + vertex_buffer, + vertex_array, + screen_texture_location, + }) + } + + unsafe fn set_location(&self, location: InclusiveRectangle) { + self.gl.viewport( + location.left as i32, + location.top as i32, + location.right as i32, + location.bottom as i32, + ); + } + + unsafe fn draw_texture(&self, texture: Texture) { + let gl = &self.gl; + gl.use_program(Some(self.program)); + + gl.uniform_1_i32(Some(&self.screen_texture_location), 0); + gl.active_texture(TEXTURE0); + gl.bind_texture(TEXTURE_2D, Some(texture)); + gl.generate_mipmap(TEXTURE_2D); + + gl.bind_vertex_array(Some(self.vertex_array)); + gl.bind_buffer(ARRAY_BUFFER, Some(self.vertex_buffer)); + gl.draw_arrays(glow::TRIANGLES, 0, 6); + } +} + +impl Drop for TextureShaderProgram { + fn drop(&mut self) { + unsafe { + self.gl.delete_program(self.program); + self.gl.delete_buffer(self.vertex_buffer); + self.gl.delete_vertex_array(self.vertex_array); + } + } +} + +pub struct AvcShaderProgram { + gl: Arc, + program: Program, + main: DrawingTextures, + aux: Option, + width: i32, + height: i32, + vertex_buffer: Buffer, + vertex_array: VertexArray, + a_position: u32, + stride_scale_location: UniformLocation, +} + +impl Drop for AvcShaderProgram { + fn drop(&mut self) { + unsafe { + let gl = self.gl.clone(); + gl.delete_buffer(self.vertex_buffer); + gl.delete_vertex_array(self.vertex_array); + gl.delete_program(self.program); + } + } +} + +impl AvcShaderProgram { + unsafe fn update_shader_data(&self, stride_scale: f32, regions: &[InclusiveRectangle]) { + let gl = self.gl.clone(); + // Redraw the two triangles for the region + #[rustfmt::skip] + let data = Vec::::from_iter(regions.iter().flat_map(|region| { + let left = region.left as f32; + let right = region.right as f32; + let top = self.height as f32 - region.bottom as f32; + let bottom = self.height as f32 - region.top as f32; + vec![ + left, top, + right, top, + right, bottom, + right, bottom, + left, bottom, + left, top, + ] + })); + + gl.bind_buffer(ARRAY_BUFFER, Some(self.vertex_buffer)); + gl.bind_vertex_array(Some(self.vertex_array)); + gl.enable_vertex_attrib_array(self.a_position); + gl.buffer_data_u8_slice(ARRAY_BUFFER, cast_as_bytes(data.as_ref()), DYNAMIC_DRAW); + gl.vertex_attrib_pointer_f32(self.a_position, 2, FLOAT, false, 8, 0); + + let data = vec![stride_scale, 1.0]; + gl.uniform_2_f32_slice(Some(&self.stride_scale_location), data.as_slice()); + } + + unsafe fn new( + gl_ref: Arc, + shader_version: &str, + width: i32, + height: i32, + shader_type: ShaderType, + ) -> crate::Result { + match shader_type { + ShaderType::Avc444 | ShaderType::Avc444v2 | ShaderType::Avc420 => {} + _ => return Err(crate::Error::from("Invalid shader type")), + } + + let gl = gl_ref.clone(); + let program = shader_type.create_shader(&gl, shader_version)?; + gl.use_program(Some(program)); + let stride_scale_location = gl.get_uniform_location(program, "stride_scale").unwrap(); + + let a_position = gl.get_attrib_location(program, "a_position").unwrap(); + + let screen_size_location = gl.get_uniform_location(program, "screen_size").unwrap(); + let u_projection_location = gl.get_uniform_location(program, "u_projection").unwrap(); + + let data: Vec = vec![width as f32, height as f32]; + gl.uniform_2_f32_slice(Some(&screen_size_location), data.as_slice()); + + // Projection matrix helps us map the points (0..width, 0.height) to (-1.0..1.0, -1.0..1.0) coordinates + #[rustfmt::skip] + let data: Vec = vec![ + 2.0 / width as f32, 0.0, 0.0, -1.0, + 0.0, 2.0 / height as f32, 0.0, -1.0, + 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + ]; + gl.uniform_matrix_4_f32_slice(Some(&u_projection_location), true, data.as_slice()); + + let main = DrawingTextures::new( + gl_ref.clone(), + program, + height, + "main_y_texture", + "main_u_texture", + "main_v_texture", + 0, + ); + + let aux = match shader_type { + ShaderType::Avc444 | ShaderType::Avc444v2 => Some(DrawingTextures::new( + gl_ref.clone(), + program, + height, + "aux_y_texture", + "aux_u_texture", + "aux_v_texture", + 3, + )), + ShaderType::Avc420 => None, + ShaderType::TextureShader => unreachable!(), + }; + // Set parameters that are not going to change across different programs. + // All shaders within a context share parameters and must be of the same type + // The parameters here are going to remain constant + + let vertex_buffer = gl.create_buffer().unwrap(); + let vertex_array = gl.create_vertex_array()?; + Ok(AvcShaderProgram { + gl: gl_ref, + program, + main, + aux, + width, + height, + vertex_buffer, + vertex_array, + a_position, + stride_scale_location, + }) + } + + /// # Safety + /// + /// TODO: Safety notes + pub unsafe fn draw( + &self, + main: &[u8], + aux: Option<&[u8]>, + stride_0: usize, + stride_1: usize, + regions: &Vec, + ) { + let gl = self.gl.clone(); + gl.use_program(Some(self.program)); + gl.viewport(0, 0, self.width, self.height); + + self.main.bind(&gl, main, stride_0, stride_1); + self.main.activate(&gl); + + if let Some(aux) = aux { + let aux_texture = self.aux.as_ref().unwrap(); + + aux_texture.bind(&gl, aux, stride_0, stride_1); + aux_texture.activate(&gl); + } + + // Textures are set with stride widths + // Map appropriately on the texture + let stride_scale = self.width as f32 / stride_0 as f32; + self.update_shader_data(stride_scale, regions); + + // For now there are assumptions that the stirde_1 is 1/2 stride_0 + if stride_1 != stride_0 / 2 { + panic!("Program cannot handle stride mismatch"); + } + // Each region is 6 vertices (two triangles) + gl.draw_arrays(glow::TRIANGLES, 0, regions.len() as i32 * 6); + } +} + +pub struct DrawingContext { + avc_420: AvcShaderProgram, + avc_444: AvcShaderProgram, + texture_shader: TextureShaderProgram, + offscreen_buffer: OffscreenBuffer, +} + +impl DrawingContext { + /// # Safety + /// + /// TODO: Safety notes + pub unsafe fn new( + gl_ref: Arc, + shader_version: &str, + width: i32, + height: i32, + is_v2: bool, + video_width: i32, + video_height: i32, + ) -> crate::Result { + let avc_444_shader_type = if is_v2 { + ShaderType::Avc444v2 + } else { + ShaderType::Avc444 + }; + + let texture_shader = + TextureShaderProgram::new(gl_ref.clone(), shader_version, width, height, video_width, video_height)?; + + let avc_420 = AvcShaderProgram::new( + gl_ref.clone(), + shader_version, + video_width, + video_height, + ShaderType::Avc420, + )?; + let avc_444 = AvcShaderProgram::new( + gl_ref.clone(), + shader_version, + video_width, + video_height, + avc_444_shader_type, + )?; + let offscreen_buffer = OffscreenBuffer::new(gl_ref, video_width, video_height)?; + Ok(DrawingContext { + avc_420, + avc_444, + texture_shader, + offscreen_buffer, + }) + } + + /// # Safety + /// + /// TODO: Safety notes + pub unsafe fn draw( + &self, + main: &[u8], + aux: Option<&[u8]>, + stride_0: usize, + stride_1: usize, + regions: &Vec, + ) { + let program = if aux.is_some() { &self.avc_444 } else { &self.avc_420 }; + // Draw to an offscreen buffer so that we can reutilize it on next frame paint + self.offscreen_buffer.activate(); + program.draw(main, aux, stride_0, stride_1, regions); + + self.offscreen_buffer.deactivate(); + } + + /// # Safety + /// + /// TODO: Safety notes + pub unsafe fn draw_cached(&self, location: InclusiveRectangle) { + self.texture_shader.set_location(location); + self.texture_shader.draw_texture(self.offscreen_buffer.texture); + } +} + +impl Drop for DrawingContext { + fn drop(&mut self) {} +} diff --git a/crates/ironrdp-glutin-renderer/src/lib.rs b/crates/ironrdp-glutin-renderer/src/lib.rs new file mode 100644 index 00000000..81c0a250 --- /dev/null +++ b/crates/ironrdp-glutin-renderer/src/lib.rs @@ -0,0 +1,12 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc( + html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg" +)] + +pub mod renderer; + +mod draw; +mod surface; + +type Error = Box; +type Result = std::result::Result; diff --git a/crates/ironrdp-glutin-renderer/src/renderer.rs b/crates/ironrdp-glutin-renderer/src/renderer.rs new file mode 100644 index 00000000..cadda4e9 --- /dev/null +++ b/crates/ironrdp-glutin-renderer/src/renderer.rs @@ -0,0 +1,204 @@ +use std::fmt::Debug; +use std::fs::File; +use std::path::PathBuf; +use std::sync::mpsc::{Receiver, RecvError, SendError, Sender}; +use std::sync::{mpsc, Arc, PoisonError}; +use std::thread; +use std::thread::JoinHandle; + +use glutin::dpi::PhysicalSize; +use ironrdp::pdu::dvc::gfx; +use ironrdp::pdu::dvc::gfx::{Codec1Type, ServerPdu}; +use ironrdp::pdu::geometry::Rectangle; +use thiserror::Error; + +use crate::surface::{DataBuffer, SurfaceDecoders, Surfaces}; + +#[derive(Debug)] +enum RenderEvent { + Paint((u16, DataBuffer)), + Repaint, + ServerPdu(ServerPdu), +} + +#[derive(Clone)] +struct DataRegion { + pub data: Vec, + pub regions: Vec, +} + +impl Debug for DataRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DataRegion") + .field("data_len", &self.data.len()) + .field("regions", &self.regions) + .finish() + } +} + +/// Runs the decode loop to decode any graphics PDU +fn handle_gfx_pdu( + graphic_receiver: Receiver, + gfx_dump_file: Option, + tx: Sender, +) -> Result<(), RendererError> { + let mut file = gfx_dump_file.map(|file| File::create(file).unwrap()); + let mut decoders = SurfaceDecoders::new(); + loop { + let message = graphic_receiver + .recv() + .map_err(|e| RendererError::ReceiveError(e.to_string()))?; + + if let Some(file) = file.as_mut() { + let _result = message.to_buffer(file); + }; + match &message { + ServerPdu::WireToSurface1(pdu) => { + let surface_id = pdu.surface_id; + let decoded = decoders.decode_wire_to_surface_1_pdu(pdu)?; + tx.send(RenderEvent::Paint((surface_id, decoded)))?; + } + ServerPdu::CreateSurface(pdu) => { + decoders.add(pdu.surface_id)?; + } + ServerPdu::DeleteSurface(pdu) => { + decoders.remove(pdu.surface_id)?; + } + _ => {} + }; + + if !matches!(message, ServerPdu::WireToSurface1(..)) { + tx.send(RenderEvent::ServerPdu(message))?; + } + } +} + +/// Runs the paint loop to paint the decoded PDU onto the canvas +fn handle_draw( + window: glutin::ContextWrapper, + rx: Receiver, +) -> Result<(), RendererError> { + let window = unsafe { window.make_current().unwrap() }; + let shader_version = "#version 410"; + let gl = unsafe { glow::Context::from_loader_function(|s| window.get_proc_address(s) as *const _) }; + let gl = Arc::new(gl); + let mut surfaces = Surfaces::new(); + loop { + let message = rx.recv()?; + info!("Got user event {:?}", message); + match message { + RenderEvent::Repaint => { + surfaces.flush_output(); + let result = window.swap_buffers(); + if result.is_err() { + error!("Swap buffers error: {:?}", result); + } + } + RenderEvent::Paint((surface_id, data)) => { + surfaces.draw_scene(surface_id, data)?; + } + RenderEvent::ServerPdu(pdu) => match pdu { + ServerPdu::CreateSurface(pdu) => { + surfaces.create_surface(pdu, gl.clone(), shader_version)?; + } + ServerPdu::DeleteSurface(pdu) => { + surfaces.delete_surface(pdu.surface_id); + } + ServerPdu::MapSurfaceToScaledOutput(pdu) => { + surfaces.map_surface_to_scaled_output(pdu)?; + } + ServerPdu::EndFrame(_) => { + window.window().request_redraw(); + } + ServerPdu::ResetGraphics(pdu) => { + window.window().set_inner_size(PhysicalSize { + width: pdu.width, + height: pdu.height, + }); + } + _ => { + info!("Ignore message: {:?}", pdu); + } + }, + } + } +} + +/// The renderer launches two threads to handle graphics messages. +/// The first thread takes any graphics PDU and decodes the messages. +/// The second thread paints the messages onto the canvas +pub struct Renderer { + render_proxy: Sender, + _decode_thread: JoinHandle>, + _draw_thread: JoinHandle>, +} + +impl Renderer { + pub fn new( + window: glutin::ContextWrapper, + graphic_receiver: Receiver, + gfx_dump_file: Option, + ) -> Renderer { + let (tx, rx) = mpsc::channel::(); + let tx2 = tx.clone(); + let decode_thread = thread::spawn(move || { + let result = handle_gfx_pdu(graphic_receiver, gfx_dump_file, tx2); + info!("Graphics handler result: {:?}", result); + result + }); + let draw_thread = thread::spawn(move || { + let result = handle_draw(window, rx); + info!("Draw handler result: {:?}", result); + result + }); + + Renderer { + render_proxy: tx, + _decode_thread: decode_thread, + _draw_thread: draw_thread, + } + } + + pub fn repaint(&self) -> Result<(), RendererError> { + self.render_proxy.send(RenderEvent::Repaint)?; + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum RendererError { + #[error("unable to send message on channel {0}")] + SendError(String), + #[error("unable to receive message on channel {0}")] + ReceiveError(String), + #[error("failed to decode OpenH264 stream {0}")] + OpenH264Error(#[from] openh264::Error), + #[error("graphics pipeline protocol error: {0}")] + GraphicsPipelineError(#[from] gfx::GraphicsPipelineError), + #[error("invalid surface id: {0}")] + InvalidSurfaceId(u16), + #[error("codec not supported: {0:?}")] + UnsupportedCodec(Codec1Type), + #[error("failed to decode rdp data")] + DecodeError, + #[error("lock poisoned")] + LockPoisonedError, +} + +impl From> for RendererError { + fn from(e: SendError) -> Self { + RendererError::SendError(e.to_string()) + } +} + +impl From for RendererError { + fn from(e: RecvError) -> Self { + RendererError::ReceiveError(e.to_string()) + } +} + +impl From> for RendererError { + fn from(_e: PoisonError) -> Self { + RendererError::LockPoisonedError + } +} diff --git a/crates/ironrdp-glutin-renderer/src/surface.rs b/crates/ironrdp-glutin-renderer/src/surface.rs new file mode 100644 index 00000000..aabc967b --- /dev/null +++ b/crates/ironrdp-glutin-renderer/src/surface.rs @@ -0,0 +1,335 @@ +use std::collections::HashMap; +use std::fmt::Debug; +use std::sync::Arc; + +use glow::Context; +use ironrdp::pdu::dvc::gfx::{ + Avc420BitmapStream, Avc444BitmapStream, Codec1Type, CreateSurfacePdu, Encoding, GraphicsPipelineError, PixelFormat, + WireToSurface1Pdu, +}; +use ironrdp::pdu::geometry::{ + Rectangle as _, + InclusiveRectangle, +}; +use ironrdp::pdu::PduBufferParsing; +use openh264::decoder::{DecodedYUV, Decoder}; + +use crate::draw::DrawingContext; +use crate::renderer::RendererError; + +type Result = std::result::Result; + +#[derive(Clone)] +struct DataRegion { + data: Vec, + regions: Vec, +} + +impl Debug for DataRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DataRegion") + .field("data_len", &self.data.len()) + .field("regions", &self.regions) + .finish() + } +} + +pub struct SurfaceDecoders { + decoders: HashMap, +} + +impl SurfaceDecoders { + pub fn new() -> Self { + SurfaceDecoders { + decoders: HashMap::new(), + } + } + pub fn add(&mut self, id: u16) -> Result<()> { + self.decoders.insert(id, Decoder::new()?); + Ok(()) + } + + pub fn remove(&mut self, id: u16) -> Result<()> { + self.decoders.remove(&id); + Ok(()) + } + + pub fn decode_wire_to_surface_1_pdu(&mut self, pdu: &WireToSurface1Pdu) -> Result { + let decoder = self + .decoders + .get_mut(&pdu.surface_id) + .ok_or(RendererError::InvalidSurfaceId(pdu.surface_id))?; + match pdu.codec_id { + ironrdp::pdu::dvc::gfx::Codec1Type::Avc420 => { + let packet = Avc420BitmapStream::from_buffer_consume(&mut pdu.bitmap_data.as_slice()) + .map_err(GraphicsPipelineError::from)?; + let yuv = decoder.decode(packet.data)?.ok_or(RendererError::DecodeError)?; + let dimensions = yuv.dimension_rgb(); + let strides = yuv.strides_yuv(); + let regions = packet.rectangles; + let data = convert_to_buffer(yuv); + let data1 = DataRegion { data, regions }; + Ok(DataBuffer { + main: Some(data1), + aux: None, + stride0: strides.0, + stride1: strides.1, + operation: Encoding::LUMA, + codec: pdu.codec_id, + dimensions, + }) + } + ironrdp::pdu::dvc::gfx::Codec1Type::Avc444 | ironrdp::pdu::dvc::gfx::Codec1Type::Avc444v2 => { + let packet = Avc444BitmapStream::from_buffer_consume(&mut pdu.bitmap_data.as_slice()) + .map_err(GraphicsPipelineError::from)?; + let yuv = decoder.decode(packet.stream1.data)?.ok_or(RendererError::DecodeError)?; + let dimensions = yuv.dimension_rgb(); + let strides = yuv.strides_yuv(); + let regions = packet.stream1.rectangles; + let data = convert_to_buffer(yuv); + let data1 = DataRegion { data, regions }; + + let data2 = if packet.encoding == Encoding::LUMA_AND_CHROMA { + let aux = packet.stream2.unwrap(); + let yuv = decoder.decode(aux.data)?.ok_or(RendererError::DecodeError)?; + let data = convert_to_buffer(yuv); + let regions = aux.rectangles; + Some(DataRegion { data, regions }) + } else { + None + }; + let data_buffer = match packet.encoding { + Encoding::LUMA_AND_CHROMA => DataBuffer { + main: Some(data1), + aux: data2, + stride0: strides.0, + stride1: strides.1, + operation: packet.encoding, + codec: pdu.codec_id, + dimensions, + }, + Encoding::LUMA => DataBuffer { + main: Some(data1), + aux: None, + stride0: strides.0, + stride1: strides.1, + operation: packet.encoding, + codec: pdu.codec_id, + dimensions, + }, + Encoding::CHROMA => DataBuffer { + main: None, + aux: Some(data1), + stride0: strides.0, + stride1: strides.1, + operation: packet.encoding, + codec: pdu.codec_id, + dimensions, + }, + _ => unreachable!("Unknown encoding type"), + }; + Ok(data_buffer) + } + _ => Err(RendererError::UnsupportedCodec(pdu.codec_id)), + } + } +} + +#[derive(Debug, Clone)] +pub struct DataBuffer { + operation: Encoding, + main: Option, + aux: Option, + stride0: usize, + stride1: usize, + codec: Codec1Type, + dimensions: (usize, usize), +} + +pub struct Surface { + id: u16, + _pixel_format: PixelFormat, + context: Option, + location: Option, + data_cache: Option, + shader_version: String, + gl: Arc, + width: u16, + height: u16, +} + +impl Surface { + pub fn new( + id: u16, + pixel_format: PixelFormat, + gl: Arc, + shader_version: &str, + width: u16, + height: u16, + ) -> Result { + Ok(Surface { + id, + _pixel_format: pixel_format, + context: None, + location: None, + data_cache: None, + gl, + width, + height, + shader_version: shader_version.to_string(), + }) + } + + pub fn set_location(&mut self, location: InclusiveRectangle) { + self.location = Some(location); + } + + fn draw_scene(&mut self, data: DataBuffer) -> Result<()> { + let stride0 = data.stride0; + let stride1 = data.stride1; + let (main_data, main_regions) = if let Some(data) = data.main.as_ref() { + self.data_cache = Some(data.clone()); + (data.data.as_slice(), &data.regions) + } else { + let cache = self.data_cache.as_ref().unwrap(); + (cache.data.as_slice(), &cache.regions) + }; + let (aux_data, regions) = if data.operation == Encoding::CHROMA || data.operation == Encoding::LUMA_AND_CHROMA { + let aux = data.aux.as_ref().unwrap(); + (Some(aux.data.as_slice()), &aux.regions) + } else { + (None, main_regions) + }; + unsafe { + let context = if let Some(context) = self.context.as_mut() { + context + } else { + self.context = Some( + DrawingContext::new( + self.gl.clone(), + &self.shader_version, + self.width as i32, + self.height as i32, + data.codec == Codec1Type::Avc444v2, + data.dimensions.0 as i32, + data.dimensions.1 as i32, + ) + .expect("Initiliazation of context failed"), + ); + self.context.as_mut().unwrap() + }; + match data.operation { + Encoding::LUMA_AND_CHROMA => { + context.draw(main_data, aux_data, stride0, stride1, regions); + } + Encoding::LUMA => { + context.draw(main_data, None, stride0, stride1, regions); + } + Encoding::CHROMA => { + context.draw(main_data, aux_data, stride0, stride1, regions); + } + _ => { + error!("Unknown operation type"); + } + } + } + Ok(()) + } + + fn draw_cached(&self) { + if let Some(context) = self.context.as_ref() { + let location = if let Some(location) = self.location.as_ref() { + location.clone() + } else { + InclusiveRectangle { + left: 0, + top: 0, + right: self.width - 1, + bottom: self.height - 1, + } + }; + + unsafe { + context.draw_cached(location); + } + } + } +} + +pub struct Surfaces { + surfaces: HashMap, +} + +impl Surfaces { + pub(crate) fn new() -> Self { + Surfaces { + surfaces: HashMap::new(), + } + } + + fn get_surface(&mut self, id: u16) -> Result<&mut Surface> { + self.surfaces.get_mut(&id).ok_or(RendererError::InvalidSurfaceId(id)) + } + + pub(crate) fn create_surface( + &mut self, + pdu: CreateSurfacePdu, + gl: Arc, + shader_version: &str, + ) -> Result<()> { + let surface = Surface::new( + pdu.surface_id, + pdu.pixel_format, + gl, + shader_version, + pdu.width, + pdu.height, + )?; + self.surfaces.insert(surface.id, surface); + Ok(()) + } + + pub(crate) fn delete_surface(&mut self, id: u16) { + self.surfaces.remove(&id); + } + + pub(crate) fn draw_scene(&mut self, id: u16, data: DataBuffer) -> Result<()> { + let surface = self.get_surface(id)?; + surface.draw_scene(data) + } + + pub(crate) fn flush_output(&mut self) { + for (_id, surface) in self.surfaces.iter_mut() { + surface.draw_cached(); + } + } + + pub(crate) fn map_surface_to_scaled_output( + &mut self, + pdu: ironrdp::pdu::dvc::gfx::MapSurfaceToScaledOutputPdu, + ) -> Result<()> { + let surface = self.get_surface(pdu.surface_id)?; + surface.set_location(InclusiveRectangle { + left: pdu.output_origin_x as u16, + top: pdu.output_origin_y as u16, + right: pdu.target_width as u16, + bottom: pdu.target_height as u16, + }); + Ok(()) + } +} + +/// Convert the decoded data to a buffer. OpenH264 documentation says that if +/// the data is not immediately used it should be copied out. +fn convert_to_buffer(yuv: DecodedYUV) -> Vec { + let y = yuv.y_with_stride(); + let u = yuv.u_with_stride(); + let v = yuv.v_with_stride(); + let total_len = y.len() + u.len() + v.len(); + let mut data = vec![0; total_len]; + let data_slice = data.as_mut_slice(); + data_slice[0..y.len()].copy_from_slice(&y[0..]); + data_slice[y.len()..y.len() + u.len()].copy_from_slice(&u[0..]); + data_slice[y.len() + u.len()..y.len() + u.len() + v.len()].copy_from_slice(&v[0..]); + data +} diff --git a/crates/ironrdp-graphics/CHANGELOG.md b/crates/ironrdp-graphics/CHANGELOG.md new file mode 100644 index 00000000..1a7bfd3c --- /dev/null +++ b/crates/ironrdp-graphics/CHANGELOG.md @@ -0,0 +1,66 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.6.0...ironrdp-graphics-v0.7.0)] - 2025-12-18 + +### Added + +- [**breaking**] `InvalidIntegralConversion` variant in `RlgrError` and `ZgfxError` + +### Build + +- Bump bytemuck from 1.23.2 to 1.24.0 ([#1008](https://github.com/Devolutions/IronRDP/issues/1008)) ([a24a1fa9e8](https://github.com/Devolutions/IronRDP/commit/a24a1fa9e8f1898b2fcdd41d87660ab9e38f89ed)) + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.5.0...ironrdp-graphics-v0.6.0)] - 2025-06-27 + +### Bug Fixes + +- `to_64x64_ycbcr_tile` now returns a `Result` + +## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.4.0...ironrdp-graphics-v0.4.1)] - 2025-06-27 + +### Build + +- Bump the patch group across 1 directory with 3 updates (#816) ([5c5f441bdd](https://github.com/Devolutions/IronRDP/commit/5c5f441bdd514d3fe6a29b4df872709167a9916d)) + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.3.0...ironrdp-graphics-v0.4.0)] - 2025-05-27 + +### Features + +- Add helper to find diff between images ([20581bb6f1](https://github.com/Devolutions/IronRDP/commit/20581bb6f12561e22031ce0e233daeada836ea67)) + + Add some helper to find "damaged" regions, as 64x64 tiles. + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.2.0...ironrdp-graphics-v0.3.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.1.2...ironrdp-graphics-v0.2.0)] - 2025-03-07 + +### Performance + +- Replace hand-coded yuv/rgb with yuvutils ([5f1c44027a](https://github.com/Devolutions/IronRDP/commit/5f1c44027a7f6da5271565461764dd3f61729ee4)) + + cargo bench: + to_ycbcr time: [2.2988 µs 2.3251 µs 2.3517 µs] + change: [-83.643% -83.534% -83.421%] (p = 0.00 < 0.05) + Performance has improved. + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.1.1...ironrdp-graphics-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.1.0...ironrdp-graphics-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-graphics/Cargo.toml b/crates/ironrdp-graphics/Cargo.toml new file mode 100644 index 00000000..938612c1 --- /dev/null +++ b/crates/ironrdp-graphics/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "ironrdp-graphics" +version = "0.7.0" +readme = "README.md" +description = "RDP image processing primitives" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +# test = false + +[dependencies] +bit_field = "0.10" +bitflags = "2.9" +bitvec = "1.0" +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6", features = ["std"] } # public +byteorder = "1.5" # TODO: remove +num-derive.workspace = true # TODO: remove +num-traits.workspace = true # TODO: remove +yuv = { version = "0.8", features = ["rdp"] } + +[dev-dependencies] +bmp = "0.5" +bytemuck = "1.24" +expect-test.workspace = true + +[lints] +workspace = true + diff --git a/crates/ironrdp-graphics/LICENSE-APACHE b/crates/ironrdp-graphics/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-graphics/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-graphics/LICENSE-MIT b/crates/ironrdp-graphics/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-graphics/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-graphics/README.md b/crates/ironrdp-graphics/README.md new file mode 100644 index 00000000..ac5c92bb --- /dev/null +++ b/crates/ironrdp-graphics/README.md @@ -0,0 +1,7 @@ +# IronRDP Graphics + +Image processing primitives and algorithms for RDP (ZGFX, DWT…). + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-graphics/src/color_conversion.rs b/crates/ironrdp-graphics/src/color_conversion.rs new file mode 100644 index 00000000..2ec929b5 --- /dev/null +++ b/crates/ironrdp-graphics/src/color_conversion.rs @@ -0,0 +1,136 @@ +use std::io; + +use yuv::{ + rdp_abgr_to_yuv444, rdp_argb_to_yuv444, rdp_bgra_to_yuv444, rdp_rgba_to_yuv444, rdp_yuv444_to_argb, + rdp_yuv444_to_rgba, BufferStoreMut, YuvError, YuvPlanarImage, YuvPlanarImageMut, +}; + +use crate::image_processing::PixelFormat; + +// FIXME: used for the test suite, we may want to drop it +pub fn ycbcr_to_argb(input: YCbCrBuffer<'_>, output: &mut [u8]) -> io::Result<()> { + let len = u32::try_from(output.len()).map_err(io::Error::other)?; + let width = len / 4; + let planar = YuvPlanarImage { + y_plane: input.y, + y_stride: width, + u_plane: input.cb, + u_stride: width, + v_plane: input.cr, + v_stride: width, + width, + height: 1, + }; + rdp_yuv444_to_argb(&planar, output, len).map_err(io::Error::other) +} + +pub fn ycbcr_to_rgba(input: YCbCrBuffer<'_>, output: &mut [u8]) -> io::Result<()> { + let len = u32::try_from(output.len()).map_err(io::Error::other)?; + let width = len / 4; + let planar = YuvPlanarImage { + y_plane: input.y, + y_stride: width, + u_plane: input.cb, + u_stride: width, + v_plane: input.cr, + v_stride: width, + width, + height: 1, + }; + rdp_yuv444_to_rgba(&planar, output, len).map_err(io::Error::other) +} + +/// # Panics +/// +/// - Panics if `width` > 64. +/// - Panics if `height` > 64. +#[expect(clippy::too_many_arguments)] +pub fn to_64x64_ycbcr_tile( + input: &[u8], + width: u32, + height: u32, + stride: u32, + format: PixelFormat, + y: &mut [i16; 64 * 64], + cb: &mut [i16; 64 * 64], + cr: &mut [i16; 64 * 64], +) -> Result<(), YuvError> { + assert!(width <= 64); + assert!(height <= 64); + + let y_plane = BufferStoreMut::Borrowed(y); + let u_plane = BufferStoreMut::Borrowed(cb); + let v_plane = BufferStoreMut::Borrowed(cr); + let mut plane = YuvPlanarImageMut { + y_plane, + y_stride: 64, + u_plane, + u_stride: 64, + v_plane, + v_stride: 64, + width, + height, + }; + + match format { + PixelFormat::RgbA32 | PixelFormat::RgbX32 => rdp_rgba_to_yuv444(&mut plane, input, stride), + PixelFormat::ARgb32 | PixelFormat::XRgb32 => rdp_argb_to_yuv444(&mut plane, input, stride), + PixelFormat::BgrA32 | PixelFormat::BgrX32 => rdp_bgra_to_yuv444(&mut plane, input, stride), + PixelFormat::ABgr32 | PixelFormat::XBgr32 => rdp_abgr_to_yuv444(&mut plane, input, stride), + } +} + +/// Convert a 16-bit RDP color to RGB representation. Input value should be represented in +/// little-endian format. +pub fn rdp_16bit_to_rgb(color: u16) -> [u8; 3] { + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer underflow)")] + let out = { + let r = u8::try_from(((((color >> 11) & 0x1f) * 527) + 23) >> 6).expect("max possible value is 255"); + let g = u8::try_from(((((color >> 5) & 0x3f) * 259) + 33) >> 6).expect("max possible value is 255"); + let b = u8::try_from((((color & 0x1f) * 527) + 23) >> 6).expect("max possible value is 255"); + [r, g, b] + }; + + out +} + +#[derive(Debug)] +pub struct YCbCrBuffer<'a> { + pub y: &'a [i16], + pub cb: &'a [i16], + pub cr: &'a [i16], +} + +impl Iterator for YCbCrBuffer<'_> { + type Item = YCbCr; + + fn next(&mut self) -> Option { + if !self.y.is_empty() && !self.cb.is_empty() && !self.cr.is_empty() { + let y = self.y[0]; + let cb = self.cb[0]; + let cr = self.cr[0]; + + self.y = &self.y[1..]; + self.cb = &self.cb[1..]; + self.cr = &self.cr[1..]; + + Some(YCbCr { y, cb, cr }) + } else { + None + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct YCbCr { + pub y: i16, + pub cb: i16, + pub cr: i16, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Rgb { + pub r: u8, + pub g: u8, + pub b: u8, +} diff --git a/crates/ironrdp-graphics/src/diff.rs b/crates/ironrdp-graphics/src/diff.rs new file mode 100644 index 00000000..1a45c48b --- /dev/null +++ b/crates/ironrdp-graphics/src/diff.rs @@ -0,0 +1,334 @@ +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct Rect { + pub x: usize, + pub y: usize, + pub width: usize, + pub height: usize, +} + +impl Rect { + pub fn new(x: usize, y: usize, width: usize, height: usize) -> Self { + Self { x, y, width, height } + } + + #[must_use] + pub fn add_xy(mut self, x: usize, y: usize) -> Self { + self.x += x; + self.y += y; + self + } + + fn intersect(&self, other: &Rect) -> Option { + let x = self.x.max(other.x); + let y = self.y.max(other.y); + let width = (self.x + self.width).min(other.x + other.width); + if width <= x { + return None; + } + let width = width - x; + let height = (self.y + self.height).min(other.y + other.height); + if height <= y { + return None; + } + let height = height - y; + + Some(Rect::new(x, y, width, height)) + } +} + +const TILE_SIZE: usize = 64; + +fn find_different_tiles( + image1: &[u8], + stride1: usize, + image2: &[u8], + stride2: usize, + width: usize, + height: usize, +) -> Vec { + assert!(stride1 >= width * BPP); + assert!(stride2 >= width * BPP); + assert!(image1.len() >= (height - 1) * stride1 + width * BPP); + assert!(image2.len() >= (height - 1) * stride2 + width * BPP); + + let tiles_x = width.div_ceil(TILE_SIZE); + let tiles_y = height.div_ceil(TILE_SIZE); + let mut tile_differences = vec![false; tiles_x * tiles_y]; + + tile_differences.iter_mut().enumerate().for_each(|(idx, diff)| { + let tile_start_x = (idx % tiles_x) * TILE_SIZE; + let tile_end_x = (tile_start_x + TILE_SIZE).min(width); + let tile_start_y = (idx / tiles_x) * TILE_SIZE; + let tile_end_y = (tile_start_y + TILE_SIZE).min(height); + + // Check for any difference in tile using slice comparisons + let has_diff = (tile_start_y..tile_end_y).any(|y| { + let row_start1 = y * stride1; + let row_start2 = y * stride2; + let tile_row_start1 = row_start1 + tile_start_x * BPP; + let tile_row_end1 = row_start1 + tile_end_x * BPP; + let tile_row_start2 = row_start2 + tile_start_x * BPP; + let tile_row_end2 = row_start2 + tile_end_x * BPP; + + image1[tile_row_start1..tile_row_end1] != image2[tile_row_start2..tile_row_end2] + }); + + *diff = has_diff; + }); + + tile_differences +} + +fn find_different_rects( + image1: &[u8], + stride1: usize, + image2: &[u8], + stride2: usize, + width: usize, + height: usize, +) -> Vec { + let mut tile_differences = find_different_tiles::(image1, stride1, image2, stride2, width, height); + + let mod_width = width % TILE_SIZE; + let mod_height = height % TILE_SIZE; + let tiles_x = width.div_ceil(TILE_SIZE); + let tiles_y = height.div_ceil(TILE_SIZE); + + let mut rectangles = Vec::new(); + let mut current_idx = 0; + let total_tiles = tiles_x * tiles_y; + + // Process tiles in linear fashion to find rectangular regions + while current_idx < total_tiles { + if !tile_differences[current_idx] { + current_idx += 1; + continue; + } + + let start_y = current_idx / tiles_x; + let start_x = current_idx % tiles_x; + + // Expand horizontally as much as possible + let mut max_width = 1; + while start_x + max_width < tiles_x && tile_differences[current_idx + max_width] { + max_width += 1; + } + + // Expand vertically as much as possible + let mut max_height = 1; + 'vertical: while start_y + max_height < tiles_y { + for x in 0..max_width { + let check_idx = (start_y + max_height) * tiles_x + start_x + x; + if !tile_differences[check_idx] { + break 'vertical; + } + } + max_height += 1; + } + + // Calculate pixel coordinates + let pixel_x = start_x * TILE_SIZE; + let pixel_y = start_y * TILE_SIZE; + + let pixel_width = if start_x + max_width == tiles_x && mod_width > 0 { + (max_width - 1) * TILE_SIZE + mod_width + } else { + max_width * TILE_SIZE + }; + + let pixel_height = if start_y + max_height == tiles_y && mod_height > 0 { + (max_height - 1) * TILE_SIZE + mod_height + } else { + max_height * TILE_SIZE + }; + + rectangles.push(Rect { + x: pixel_x, + y: pixel_y, + width: pixel_width, + height: pixel_height, + }); + + // Mark tiles as processed + for y in 0..max_height { + for x in 0..max_width { + let idx = (start_y + y) * tiles_x + start_x + x; + tile_differences[idx] = false; + } + } + + current_idx += max_width; + } + + rectangles +} + +/// Helper function to find different regions in two images. +/// +/// This function takes two images as input and returns a list of rectangles +/// representing the different regions between the two images, in image2 coordinates. +/// +/// ```text +/// ┌───────────────────────────────────────────┐ +/// │ image1 │ +/// │ │ +/// │ (x,y) │ +/// │ ┌───────────────┐ │ +/// │ │ image2 │ │ +/// │ │ │ │ +/// │ │ │ │ +/// │ │ │ │ +/// │ │ │ │ +/// │ │ │ │ +/// │ └───────────────┘ │ +/// │ │ +/// └───────────────────────────────────────────┘ +/// ``` +#[expect(clippy::too_many_arguments)] +pub fn find_different_rects_sub( + image1: &[u8], + stride1: usize, + width1: usize, + height1: usize, + image2: &[u8], + stride2: usize, + width2: usize, + height2: usize, + x: usize, + y: usize, +) -> Vec { + let rect1 = Rect::new(0, 0, width1, height1); + let rect2 = Rect::new(x, y, width2, height2); + let Some(inter) = rect1.intersect(&rect2) else { + return vec![]; + }; + + let image1 = &image1[y * stride1 + x * BPP..]; + find_different_rects::(image1, stride1, image2, stride2, inter.width, inter.height) +} + +#[cfg(test)] +mod tests { + use bytemuck::cast_slice; + + use super::*; + + #[test] + fn test_intersect() { + let r1 = Rect::new(0, 0, 640, 480); + let r2 = Rect::new(10, 10, 10, 10); + let r3 = Rect::new(630, 470, 20, 20); + + assert_eq!(r1.intersect(&r1).as_ref(), Some(&r1)); + assert_eq!(r1.intersect(&r2).as_ref(), Some(&r2)); + assert_eq!(r1.intersect(&r3), Some(Rect::new(630, 470, 10, 10))); + assert_eq!(r2.intersect(&r3), None); + } + + #[test] + fn test_single_tile() { + const SIZE: usize = 128; + let image1 = vec![0u32; SIZE * SIZE]; + let mut image2 = vec![0u32; SIZE * SIZE]; + image2[65 * 128 + 65] = 1; + let result = + find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE); + assert_eq!( + result, + vec![Rect { + x: 64, + y: 64, + width: 64, + height: 64 + }] + ); + } + + #[test] + fn test_adjacent_tiles() { + const SIZE: usize = 256; + let image1 = vec![0u32; SIZE * SIZE]; + let mut image2 = vec![0u32; SIZE * SIZE]; + // Modify two adjacent tiles + image2[65 * SIZE + 65] = 1; + image2[65 * SIZE + 129] = 1; + let result = + find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE); + assert_eq!( + result, + vec![Rect { + x: 64, + y: 64, + width: 128, + height: 64 + }] + ); + } + + #[test] + fn test_edge_tiles() { + const SIZE: usize = 100; + let image1 = vec![0u32; SIZE * SIZE]; + let mut image2 = vec![0u32; SIZE * SIZE]; + image2[65 * SIZE + 65] = 1; + let result = + find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE); + assert_eq!( + result, + vec![Rect { + x: 64, + y: 64, + width: 36, + height: 36 + }] + ); + } + + #[test] + fn test_large() { + const SIZE: usize = 4096; + let image1 = vec![0u32; SIZE * SIZE]; + let mut image2 = vec![0u32; SIZE * SIZE]; + image2[95 * 100 + 95] = 1; + let _result = + find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE); + } + + #[test] + fn test_sub_diff() { + let image1 = vec![0u32; 2048 * 2048]; + let mut image2 = vec![0u32; 1024 * 1024]; + image2[0] = 1; + image2[1024 * 65 + 512 - 1] = 1; + + let res = find_different_rects_sub::<4>( + cast_slice(&image1), + 2048 * 4, + 2048, + 2048, + cast_slice(&image2), + 1024 * 4, + 512, + 512, + 1024, + 1024, + ); + assert_eq!( + res, + vec![ + Rect { + x: 0, + y: 0, + width: 64, + height: 64 + }, + Rect { + x: 448, + y: 64, + width: 64, + height: 64 + } + ] + ) + } +} diff --git a/crates/ironrdp-graphics/src/dwt.rs b/crates/ironrdp-graphics/src/dwt.rs new file mode 100644 index 00000000..5e13b8df --- /dev/null +++ b/crates/ironrdp-graphics/src/dwt.rs @@ -0,0 +1,217 @@ +use ironrdp_pdu::utils::SplitTo as _; + +pub fn encode(buffer: &mut [i16], temp_buffer: &mut [i16]) { + encode_block::<32>(&mut *buffer, temp_buffer); + encode_block::<16>(&mut buffer[3072..], temp_buffer); + encode_block::<8>(&mut buffer[3840..], temp_buffer); +} + +fn encode_block(buffer: &mut [i16], temp_buffer: &mut [i16]) { + dwt_vertical::(buffer, temp_buffer); + dwt_horizontal::(buffer, temp_buffer); +} + +// DWT in vertical direction, results in 2 sub-bands in L, H order in tmp buffer dwt. +fn dwt_vertical(buffer: &[i16], dwt: &mut [i16]) { + let total_width = SUBBAND_WIDTH * 2; + + for x in 0..total_width { + for n in 0..SUBBAND_WIDTH { + let y = n * 2; + let l_index = n * total_width + x; + let h_index = l_index + SUBBAND_WIDTH * total_width; + let src_index = y * total_width + x; + + dwt[h_index] = i32_to_i16_possible_truncation( + (i32::from(buffer[src_index + total_width]) + - ((i32::from(buffer[src_index]) + + i32::from(buffer[src_index + if n < SUBBAND_WIDTH - 1 { 2 * total_width } else { 0 }])) + >> 1)) + >> 1, + ); + dwt[l_index] = i32_to_i16_possible_truncation( + i32::from(buffer[src_index]) + + if n == 0 { + i32::from(dwt[h_index]) + } else { + (i32::from(dwt[h_index - total_width]) + i32::from(dwt[h_index])) >> 1 + }, + ); + } + } +} + +// DWT in horizontal direction, results in 4 sub-bands in HL(0), LH(1), HH(2), +// LL(3) order, stored in original buffer. +// The lower part L generates LL(3) and HL(0). +// The higher part H generates LH(1) and HH(2). +fn dwt_horizontal(mut buffer: &mut [i16], dwt: &[i16]) { + let total_width = SUBBAND_WIDTH * 2; + let squared_subband_width = SUBBAND_WIDTH.pow(2); + + let mut hl = buffer.split_to(squared_subband_width); + let mut lh = buffer.split_to(squared_subband_width); + let mut hh = buffer.split_to(squared_subband_width); + let mut ll = buffer; + let (mut l_src, mut h_src) = dwt.split_at(squared_subband_width * 2); + + for _ in 0..SUBBAND_WIDTH { + // L + for n in 0..SUBBAND_WIDTH { + let x = n * 2; + + // HL + hl[n] = i32_to_i16_possible_truncation( + (i32::from(l_src[x + 1]) + - ((i32::from(l_src[x]) + i32::from(l_src[if n < SUBBAND_WIDTH - 1 { x + 2 } else { x }])) >> 1)) + >> 1, + ); + // LL + ll[n] = i32_to_i16_possible_truncation( + i32::from(l_src[x]) + + if n == 0 { + i32::from(hl[n]) + } else { + (i32::from(hl[n - 1]) + i32::from(hl[n])) >> 1 + }, + ); + } + + // H + for n in 0..SUBBAND_WIDTH { + let x = n * 2; + + // HH + hh[n] = i32_to_i16_possible_truncation( + (i32::from(h_src[x + 1]) + - ((i32::from(h_src[x]) + i32::from(h_src[if n < SUBBAND_WIDTH - 1 { x + 2 } else { x }])) >> 1)) + >> 1, + ); + // LH + lh[n] = i32_to_i16_possible_truncation( + i32::from(h_src[x]) + + if n == 0 { + i32::from(hh[n]) + } else { + (i32::from(hh[n - 1]) + i32::from(hh[n])) >> 1 + }, + ); + } + + hl = &mut hl[SUBBAND_WIDTH..]; + lh = &mut lh[SUBBAND_WIDTH..]; + hh = &mut hh[SUBBAND_WIDTH..]; + ll = &mut ll[SUBBAND_WIDTH..]; + + l_src = &l_src[total_width..]; + h_src = &h_src[total_width..]; + } +} + +pub fn decode(buffer: &mut [i16], temp_buffer: &mut [i16]) { + decode_block(&mut buffer[3840..], temp_buffer, 8); + decode_block(&mut buffer[3072..], temp_buffer, 16); + decode_block(&mut *buffer, temp_buffer, 32); +} + +fn decode_block(buffer: &mut [i16], temp_buffer: &mut [i16], subband_width: usize) { + inverse_horizontal(buffer, temp_buffer, subband_width); + inverse_vertical(buffer, temp_buffer, subband_width); +} + +// Inverse DWT in horizontal direction, results in 2 sub-bands in L, H order in output buffer +// The 4 sub-bands are stored in HL(0), LH(1), HH(2), LL(3) order. +// The lower part L uses LL(3) and HL(0). +// The higher part H uses LH(1) and HH(2). +fn inverse_horizontal(mut buffer: &[i16], temp_buffer: &mut [i16], subband_width: usize) { + let total_width = subband_width * 2; + let squared_subband_width = subband_width.pow(2); + + let mut hl = buffer.split_to(squared_subband_width); + let mut lh = buffer.split_to(squared_subband_width); + let mut hh = buffer.split_to(squared_subband_width); + let mut ll = buffer; + + let (mut l_dst, mut h_dst) = temp_buffer.split_at_mut(squared_subband_width * 2); + + for _ in 0..subband_width { + // Even coefficients + l_dst[0] = i32_to_i16_possible_truncation(i32::from(ll[0]) - ((i32::from(hl[0]) + i32::from(hl[0]) + 1) >> 1)); + h_dst[0] = i32_to_i16_possible_truncation(i32::from(lh[0]) - ((i32::from(hh[0]) + i32::from(hh[0]) + 1) >> 1)); + for n in 1..subband_width { + let x = n * 2; + l_dst[x] = + i32_to_i16_possible_truncation(i32::from(ll[n]) - ((i32::from(hl[n - 1]) + i32::from(hl[n]) + 1) >> 1)); + h_dst[x] = + i32_to_i16_possible_truncation(i32::from(lh[n]) - ((i32::from(hh[n - 1]) + i32::from(hh[n]) + 1) >> 1)); + } + + // Odd coefficients + for n in 0..subband_width - 1 { + let x = n * 2; + l_dst[x + 1] = i32_to_i16_possible_truncation( + i32::from(hl[n] << 1) + ((i32::from(l_dst[x]) + i32::from(l_dst[x + 2])) >> 1), + ); + h_dst[x + 1] = i32_to_i16_possible_truncation( + i32::from(hh[n] << 1) + ((i32::from(h_dst[x]) + i32::from(h_dst[x + 2])) >> 1), + ); + } + let n = subband_width - 1; + let x = n * 2; + l_dst[x + 1] = i32_to_i16_possible_truncation(i32::from(hl[n] << 1) + i32::from(l_dst[x])); + h_dst[x + 1] = i32_to_i16_possible_truncation(i32::from(hh[n] << 1) + i32::from(h_dst[x])); + + hl = &hl[subband_width..]; + lh = &lh[subband_width..]; + hh = &hh[subband_width..]; + ll = &ll[subband_width..]; + + l_dst = &mut l_dst[total_width..]; + h_dst = &mut h_dst[total_width..]; + } +} + +fn inverse_vertical(mut buffer: &mut [i16], mut temp_buffer: &[i16], subband_width: usize) { + let total_width = subband_width * 2; + + for _ in 0..total_width { + buffer[0] = i32_to_i16_possible_truncation( + i32::from(temp_buffer[0]) - ((i32::from(temp_buffer[subband_width * total_width]) * 2 + 1) >> 1), + ); + + let mut l = temp_buffer; + let mut lh = &temp_buffer[(subband_width - 1) * total_width..]; + let mut h = &temp_buffer[subband_width * total_width..]; + let mut dst = &mut *buffer; + + for _ in 1..subband_width { + l = &l[total_width..]; + lh = &lh[total_width..]; + h = &h[total_width..]; + + // Even coefficients + dst[2 * total_width] = + i32_to_i16_possible_truncation(i32::from(l[0]) - ((i32::from(lh[0]) + i32::from(h[0]) + 1) >> 1)); + + // Odd coefficients + dst[total_width] = i32_to_i16_possible_truncation( + i32::from(lh[0] << 1) + ((i32::from(dst[0]) + i32::from(dst[2 * total_width])) >> 1), + ); + + dst = &mut dst[2 * total_width..]; + } + + dst[total_width] = i32_to_i16_possible_truncation( + i32::from(lh[total_width] << 1) + ((i32::from(dst[0]) + i32::from(dst[0])) >> 1), + ); + + temp_buffer = &temp_buffer[1..]; + buffer = &mut buffer[1..]; + } +} + +#[expect(clippy::as_conversions)] +#[expect(clippy::cast_possible_truncation)] +fn i32_to_i16_possible_truncation(value: i32) -> i16 { + value as i16 +} diff --git a/crates/ironrdp-graphics/src/image_processing.rs b/crates/ironrdp-graphics/src/image_processing.rs new file mode 100644 index 00000000..501424ee --- /dev/null +++ b/crates/ironrdp-graphics/src/image_processing.rs @@ -0,0 +1,302 @@ +use core::{cmp, fmt}; +use std::io; + +use byteorder::WriteBytesExt as _; +use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; + +const ALPHA_OPAQUE: u8 = 0xff; + +pub struct ImageRegionMut<'a> { + pub region: InclusiveRectangle, + pub step: u16, + pub pixel_format: PixelFormat, + pub data: &'a mut [u8], +} + +impl fmt::Debug for ImageRegionMut<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ImageRegionMut") + .field("region", &self.region) + .field("step", &self.step) + .field("pixel_format", &self.pixel_format) + .field("data_len", &self.data.len()) + .finish() + } +} + +pub struct ImageRegion<'a> { + pub region: InclusiveRectangle, + pub step: u16, + pub pixel_format: PixelFormat, + pub data: &'a [u8], +} + +impl fmt::Debug for ImageRegion<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ImageRegion") + .field("region", &self.region) + .field("step", &self.step) + .field("pixel_format", &self.pixel_format) + .field("data_len", &self.data.len()) + .finish() + } +} + +impl ImageRegion<'_> { + pub fn copy_to(&self, other: &mut ImageRegionMut<'_>) -> io::Result<()> { + let width = cmp::min(self.region.width(), other.region.width()); + let height = cmp::min(self.region.height(), other.region.height()); + let width = usize::from(width); + let height = usize::from(height); + + let dst_point = Point { + x: usize::from(other.region.left), + y: usize::from(other.region.top), + }; + let src_point = Point { + x: usize::from(self.region.left), + y: usize::from(self.region.top), + }; + + let src_byte = usize::from(self.pixel_format.bytes_per_pixel()); + let dst_byte = usize::from(other.pixel_format.bytes_per_pixel()); + + let src_step = if self.step == 0 { + usize::from(self.region.width()) * src_byte + } else { + usize::from(self.step) + }; + let dst_step = if other.step == 0 { + width * dst_byte + } else { + usize::from(other.step) + }; + + if self.pixel_format.eq_no_alpha(other.pixel_format) { + let width = width * dst_byte; + for y in 0..height { + let src_start = (y + src_point.y) * src_step + src_point.x * src_byte; + let dst_start = (y + dst_point.y) * dst_step + dst_point.x * dst_byte; + other.data[dst_start..dst_start + width].clone_from_slice(&self.data[src_start..src_start + width]); + } + } else { + for y in 0..height { + let src = &self.data[((y + src_point.y) * src_step)..]; + let dst = &mut other.data[((y + dst_point.y) * dst_step)..]; + + for x in 0..width { + let color = self.pixel_format.read_color(&src[((x + src_point.x) * src_byte)..])?; + other + .pixel_format + .write_color(color, &mut dst[((x + dst_point.x) * dst_byte)..])?; + } + } + } + + Ok(()) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PixelFormat { + ARgb32 = 536_971_400, + XRgb32 = 536_938_632, + ABgr32 = 537_036_936, + XBgr32 = 537_004_168, + BgrA32 = 537_168_008, + BgrX32 = 537_135_240, + RgbA32 = 537_102_472, + RgbX32 = 537_069_704, +} + +impl TryFrom for PixelFormat { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + 536_971_400 => Ok(PixelFormat::ARgb32), + 536_938_632 => Ok(PixelFormat::XRgb32), + 537_036_936 => Ok(PixelFormat::ABgr32), + 537_004_168 => Ok(PixelFormat::XBgr32), + 537_168_008 => Ok(PixelFormat::BgrA32), + 537_135_240 => Ok(PixelFormat::BgrX32), + 537_102_472 => Ok(PixelFormat::RgbA32), + 537_069_704 => Ok(PixelFormat::RgbX32), + _ => Err(()), + } + } +} + +impl PixelFormat { + fn as_u32(&self) -> u32 { + match self { + Self::ARgb32 => 536_971_400, + Self::XRgb32 => 536_938_632, + Self::ABgr32 => 537_036_936, + Self::XBgr32 => 537_004_168, + Self::BgrA32 => 537_168_008, + Self::BgrX32 => 537_135_240, + Self::RgbA32 => 537_102_472, + Self::RgbX32 => 537_069_704, + } + } + + pub const fn bytes_per_pixel(self) -> u8 { + match self { + Self::ARgb32 + | Self::XRgb32 + | Self::ABgr32 + | Self::XBgr32 + | Self::BgrA32 + | Self::BgrX32 + | Self::RgbA32 + | Self::RgbX32 => 4, + } + } + + pub fn eq_no_alpha(self, other: Self) -> bool { + let mask = !(8 << 12); + + (self.as_u32() & mask) == (other.as_u32() & mask) + } + + pub fn read_color(self, buffer: &[u8]) -> io::Result { + match self { + Self::ARgb32 + | Self::XRgb32 + | Self::ABgr32 + | Self::XBgr32 + | Self::BgrA32 + | Self::BgrX32 + | Self::RgbA32 + | Self::RgbX32 => { + if buffer.len() < 4 { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "input buffer is not large enough (this is a bug)", + )) + } else { + let color = &buffer[..4]; + + match self { + Self::ARgb32 => Ok(Rgba { + a: color[0], + r: color[1], + g: color[2], + b: color[3], + }), + Self::XRgb32 => Ok(Rgba { + a: ALPHA_OPAQUE, + r: color[1], + g: color[2], + b: color[3], + }), + Self::ABgr32 => Ok(Rgba { + a: color[0], + b: color[1], + g: color[2], + r: color[3], + }), + Self::XBgr32 => Ok(Rgba { + a: ALPHA_OPAQUE, + b: color[1], + g: color[2], + r: color[3], + }), + Self::BgrA32 => Ok(Rgba { + b: color[0], + g: color[1], + r: color[2], + a: color[3], + }), + Self::BgrX32 => Ok(Rgba { + b: color[0], + g: color[1], + r: color[2], + a: ALPHA_OPAQUE, + }), + Self::RgbA32 => Ok(Rgba { + r: color[0], + g: color[1], + b: color[2], + a: color[3], + }), + Self::RgbX32 => Ok(Rgba { + r: color[0], + g: color[1], + b: color[2], + a: ALPHA_OPAQUE, + }), + } + } + } + } + } + + pub fn write_color(self, color: Rgba, mut buffer: &mut [u8]) -> io::Result<()> { + match self { + Self::ARgb32 => { + buffer.write_u8(color.a)?; + buffer.write_u8(color.r)?; + buffer.write_u8(color.g)?; + buffer.write_u8(color.b)?; + } + Self::XRgb32 => { + buffer.write_u8(ALPHA_OPAQUE)?; + buffer.write_u8(color.r)?; + buffer.write_u8(color.g)?; + buffer.write_u8(color.b)?; + } + Self::ABgr32 => { + buffer.write_u8(color.a)?; + buffer.write_u8(color.b)?; + buffer.write_u8(color.g)?; + buffer.write_u8(color.r)?; + } + Self::XBgr32 => { + buffer.write_u8(ALPHA_OPAQUE)?; + buffer.write_u8(color.b)?; + buffer.write_u8(color.g)?; + buffer.write_u8(color.r)?; + } + Self::BgrA32 => { + buffer.write_u8(color.b)?; + buffer.write_u8(color.g)?; + buffer.write_u8(color.r)?; + buffer.write_u8(color.a)?; + } + Self::BgrX32 => { + buffer.write_u8(color.b)?; + buffer.write_u8(color.g)?; + buffer.write_u8(color.r)?; + buffer.write_u8(ALPHA_OPAQUE)?; + } + Self::RgbA32 => { + buffer.write_u8(color.r)?; + buffer.write_u8(color.g)?; + buffer.write_u8(color.b)?; + buffer.write_u8(color.a)?; + } + Self::RgbX32 => { + buffer.write_u8(color.r)?; + buffer.write_u8(color.g)?; + buffer.write_u8(color.b)?; + buffer.write_u8(ALPHA_OPAQUE)?; + } + } + + Ok(()) + } +} + +struct Point { + x: usize, + y: usize, +} + +pub struct Rgba { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} diff --git a/crates/ironrdp-graphics/src/lib.rs b/crates/ironrdp-graphics/src/lib.rs new file mode 100644 index 00000000..02d4104a --- /dev/null +++ b/crates/ironrdp-graphics/src/lib.rs @@ -0,0 +1,37 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![allow(clippy::arithmetic_side_effects)] // FIXME: remove + +pub mod color_conversion; +pub mod diff; +pub mod dwt; +pub mod image_processing; +pub mod pointer; +pub mod quantization; +pub mod rdp6; +pub mod rectangle_processing; +pub mod rle; +pub mod rlgr; +pub mod subband_reconstruction; +pub mod zgfx; + +mod utils; + +/// # Panics +/// +/// Panics if `input.len()` is not 4096 (64 * 46). +pub fn rfx_encode_component( + input: &mut [i16], + output: &mut [u8], + quant: &ironrdp_pdu::codecs::rfx::Quant, + mode: ironrdp_pdu::codecs::rfx::EntropyAlgorithm, +) -> Result { + assert_eq!(input.len(), 64 * 64); + + let mut temp = [0; 64 * 64]; // size = 8k, too big? + + dwt::encode(input, temp.as_mut_slice()); + quantization::encode(input, quant); + subband_reconstruction::encode(&mut input[4032..]); + rlgr::encode(mode, input, output) +} diff --git a/crates/ironrdp-graphics/src/pointer.rs b/crates/ironrdp-graphics/src/pointer.rs new file mode 100644 index 00000000..26dbdd40 --- /dev/null +++ b/crates/ironrdp-graphics/src/pointer.rs @@ -0,0 +1,437 @@ +//! This module implements logic to decode pointer PDUs into RGBA bitmaps ready for rendering. +//! +//! # References: +//! - Drawing pointers: +//! - Drawing color pointers: +//! - Drawing monochrome pointers +//! +//! +//! # Notes on xor/and masks encoding: +//! RDP's pointer representation is a bit weird. It uses two masks to represent a pointer - +//! andMask and xorMask. Xor mask is used as a base color for a pointer pixel, and andMask +//! mask is used co control pixel's full transparency (`src_color.a = 0`), full opacity +//! (`src_color.a = 255`) or pixel inversion (`dst_color.rgb = vec3(255) - dst_color.rgb`). +//! +//! Xor basks could be 1, 8, 16, 24 or 32 bits per pixel, and andMask is always 1 bit per pixel. +//! +//! Rules for decoding masks: +//! - `andMask == 0` -> dst_color Copy pixel from xorMask +//! - andMask == 1, xorMask == 0(black color) -> Transparent pixel +//! - andMask == 1, xorMask == 1(white color) -> Pixel is inverted + +use ironrdp_core::ReadCursor; +use ironrdp_pdu::pointer::{ColorPointerAttribute, LargePointerAttribute, PointerAttribute}; + +use crate::color_conversion::rdp_16bit_to_rgb; + +const SUPPORTED_COLOR_BPP: [u16; 4] = [1, 16, 24, 32]; + +#[derive(Debug)] +pub enum PointerError { + InvalidXorMaskSize { expected: usize, actual: usize }, + InvalidAndMaskSize { expected: usize, actual: usize }, + NotSupportedBpp { bpp: u16 }, + Pdu(ironrdp_pdu::PduError), +} + +impl core::fmt::Display for PointerError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + PointerError::InvalidXorMaskSize { expected, actual } => { + write!( + f, + "invalid pointer xorMask size. Expected: {expected}, actual: {actual}" + ) + } + PointerError::InvalidAndMaskSize { expected, actual } => { + write!( + f, + "invalid pointer andMask size. Expected: {expected}, actual: {actual}" + ) + } + PointerError::NotSupportedBpp { bpp } => { + write!(f, "not supported pointer bpp: {bpp}") + } + PointerError::Pdu(err) => err.fmt(f), + } + } +} + +impl core::error::Error for PointerError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + PointerError::InvalidXorMaskSize { .. } => None, + PointerError::InvalidAndMaskSize { .. } => None, + PointerError::NotSupportedBpp { .. } => None, + PointerError::Pdu(error) => error.source(), + } + } +} + +impl From for PointerError { + fn from(error: ironrdp_pdu::PduError) -> Self { + PointerError::Pdu(error) + } +} + +/// Represents RDP pointer in decoded form (color channels stored as RGBA pre-multiplied values) +#[derive(Debug)] +pub struct DecodedPointer { + pub width: u16, + pub height: u16, + pub hotspot_x: u16, + pub hotspot_y: u16, + pub bitmap_data: Vec, +} + +/// Pointer bitmap rendering target. Defines properties and format of the decoded bitmap. +#[derive(Clone, Copy, Debug)] +pub enum PointerBitmapTarget { + /// Software rendering target will produce RGBA bitmaps with premultiplied alpha. + /// + /// Colors with alpha channel set to 0x00 are always invisible no matter their color + /// component. We could take advantage of that, and use a special color to represent + /// inverted pixels. [0xFF, 0xFF, 0xFF, 0x00] is used for such purpose in software + /// rendering mode. + Software, + /// Accelerated rendering target will produce RGBA bitmaps with non-premultiplied alpha. + /// Inverted pixels will be rendered following the check pattern. + Accelerated, +} + +impl PointerBitmapTarget { + fn should_premultiply_alpha(self) -> bool { + match self { + Self::Software => true, + Self::Accelerated => false, + } + } + + fn should_invert_pixels_using_check_pattern(self) -> bool { + match self { + Self::Software => false, + Self::Accelerated => true, + } + } +} + +impl DecodedPointer { + pub fn new_invisible() -> Self { + Self { + width: 0, + height: 0, + bitmap_data: Vec::new(), + hotspot_x: 0, + hotspot_y: 0, + } + } + + pub fn decode_pointer_attribute( + src: &PointerAttribute<'_>, + target: PointerBitmapTarget, + ) -> Result { + Self::decode_pointer( + PointerData { + width: src.color_pointer.width, + height: src.color_pointer.height, + xor_bpp: src.xor_bpp, + xor_mask: src.color_pointer.xor_mask, + and_mask: src.color_pointer.and_mask, + hot_spot_x: src.color_pointer.hot_spot.x, + hot_spot_y: src.color_pointer.hot_spot.y, + }, + target, + ) + } + + pub fn decode_color_pointer_attribute( + src: &ColorPointerAttribute<'_>, + target: PointerBitmapTarget, + ) -> Result { + Self::decode_pointer( + PointerData { + width: src.width, + height: src.height, + xor_bpp: 24, + xor_mask: src.xor_mask, + and_mask: src.and_mask, + hot_spot_x: src.hot_spot.x, + hot_spot_y: src.hot_spot.y, + }, + target, + ) + } + + pub fn decode_large_pointer_attribute( + src: &LargePointerAttribute<'_>, + target: PointerBitmapTarget, + ) -> Result { + Self::decode_pointer( + PointerData { + width: src.width, + height: src.height, + xor_bpp: src.xor_bpp, + xor_mask: src.xor_mask, + and_mask: src.and_mask, + hot_spot_x: src.hot_spot.x, + hot_spot_y: src.hot_spot.y, + }, + target, + ) + } + + fn decode_pointer(data: PointerData<'_>, target: PointerBitmapTarget) -> Result { + if data.width == 0 || data.height == 0 { + return Ok(Self::new_invisible()); + } + + if !SUPPORTED_COLOR_BPP.contains(&data.xor_bpp) { + // 8bpp indexed colors are not supported yet (palette messages are not implemented) + // Other unknown bpps are not supported either + return Err(PointerError::NotSupportedBpp { bpp: data.xor_bpp }); + } + + let flip_vertical = data.xor_bpp != 1; + + let and_stride = Stride::from_bits(data.width.into()); + let xor_stride = Stride::from_bits(usize::from(data.width) * usize::from(data.xor_bpp)); + + if data.xor_mask.len() != xor_stride.length * usize::from(data.height) { + return Err(PointerError::InvalidXorMaskSize { + expected: xor_stride.length * usize::from(data.height), + actual: data.xor_mask.len(), + }); + } + + let default_and_mask = vec![0x00; and_stride.length * usize::from(data.height)]; + let mut and_mask = data.and_mask; + if and_mask.is_empty() { + and_mask = &default_and_mask; + } else if and_mask.len() != and_stride.length * usize::from(data.height) { + return Err(PointerError::InvalidAndMaskSize { + expected: and_stride.length * usize::from(data.height), + actual: data.and_mask.len(), + }); + } + + let mut bitmap_data = Vec::new(); + + for row_idx in 0..data.height { + // For non-monochrome cursors we read strides from bottom to top + let (mut xor_stride_cursor, mut and_stride_cursor) = if flip_vertical { + let xor_stride_cursor = + ReadCursor::new(&data.xor_mask[usize::from(data.height - row_idx - 1) * xor_stride.length..]); + let and_stride_cursor = + ReadCursor::new(&and_mask[usize::from(data.height - row_idx - 1) * and_stride.length..]); + (xor_stride_cursor, and_stride_cursor) + } else { + let xor_stride_cursor = ReadCursor::new(&data.xor_mask[usize::from(row_idx) * xor_stride.length..]); + let and_stride_cursor = ReadCursor::new(&and_mask[usize::from(row_idx) * and_stride.length..]); + (xor_stride_cursor, and_stride_cursor) + }; + + let mut color_reader = ColorStrideReader::new(data.xor_bpp, xor_stride)?; + let mut bitmask_reader = BitmaskStrideReader::new(and_stride); + + let compute_inverted_pixel = if target.should_invert_pixels_using_check_pattern() { + |row_idx: u16, col_idx: u16| -> [u8; 4] { + // Checkered pattern is used to represent inverted pixels. + if (row_idx + col_idx) % 2 == 0 { + [0xff, 0xff, 0xff, 0xff] + } else { + [0x00, 0x00, 0x00, 0xff] + } + } + } else { + |_, _| [0xFF, 0xFF, 0xFF, 0x00] + }; + + for col_idx in 0..data.width { + let and_bit = bitmask_reader.next_bit(&mut and_stride_cursor); + let color = color_reader.next_pixel(&mut xor_stride_cursor); + + if and_bit == 1 && color == [0, 0, 0, 0xff] { + // Force transparent pixel (The only way to get a transparent pixel with + // non-32-bit cursors) + bitmap_data.extend_from_slice(&[0, 0, 0, 0]); + } else if and_bit == 1 && color == [0xff, 0xff, 0xff, 0xff] { + // Inverted pixel. + bitmap_data.extend_from_slice(&compute_inverted_pixel(row_idx, col_idx)); + } else if target.should_premultiply_alpha() { + // Calculate premultiplied alpha via integer arithmetic + let with_premultiplied_alpha = [ + u8::try_from((u16::from(color[0]) * u16::from(color[0])) >> 8) + .expect("(u16 >> 8) fits into u8"), + u8::try_from((u16::from(color[1]) * u16::from(color[1])) >> 8) + .expect("(u16 >> 8) fits into u8"), + u8::try_from((u16::from(color[2]) * u16::from(color[2])) >> 8) + .expect("(u16 >> 8) fits into u8"), + color[3], + ]; + bitmap_data.extend_from_slice(&with_premultiplied_alpha); + } else { + bitmap_data.extend_from_slice(&color); + } + } + } + + Ok(Self { + width: data.width, + height: data.height, + bitmap_data, + hotspot_x: data.hot_spot_x, + hotspot_y: data.hot_spot_y, + }) + } +} + +#[derive(Clone, Copy)] +struct Stride { + length: usize, + data_bytes: usize, + padding: usize, +} + +impl Stride { + fn from_bits(bits: usize) -> Stride { + let length = bit_stride_size_align_u16(bits); + let data_bytes = bit_stride_size_align_u8(bits); + Stride { + length, + data_bytes, + padding: length - data_bytes, + } + } +} + +struct BitmaskStrideReader { + current_byte: u8, + read_bits: usize, + read_stide_bytes: usize, + stride_data_bytes: usize, + stride_padding: usize, +} + +impl BitmaskStrideReader { + fn new(stride: Stride) -> Self { + Self { + current_byte: 0, + read_bits: 8, + read_stide_bytes: 0, + stride_data_bytes: stride.data_bytes, + stride_padding: stride.padding, + } + } + + fn next_bit(&mut self, cursor: &mut ReadCursor<'_>) -> u8 { + if self.read_bits == 8 { + self.read_bits = 0; + + if self.read_stide_bytes == self.stride_data_bytes { + self.read_stide_bytes = 0; + cursor.read_slice(self.stride_padding); + } + + self.current_byte = cursor.read_u8(); + } + + let bit = (self.current_byte >> (7 - self.read_bits)) & 1; + self.read_bits += 1; + bit + } +} + +enum ColorStrideReader { + Color { + /// INVARIANT: `bpp == 16 || bpp == 24 || bpp == 32` + bpp: u16, + read_stide_bytes: usize, + stride_data_bytes: usize, + stride_padding: usize, + }, + Bitmask(BitmaskStrideReader), +} + +impl ColorStrideReader { + fn new(bpp: u16, stride: Stride) -> Result { + Ok(match bpp { + 1 => Self::Bitmask(BitmaskStrideReader::new(stride)), + bpp => Self::Color { + bpp: { + // Enforce the bpp == 16 || bpp == 24 || bpp == 32 invariant. + if !SUPPORTED_COLOR_BPP[1..].contains(&bpp) { + return Err(PointerError::NotSupportedBpp { bpp }); + } + + bpp + }, + read_stide_bytes: 0, + stride_data_bytes: stride.data_bytes, + stride_padding: stride.padding, + }, + }) + } + + fn next_pixel(&mut self, cursor: &mut ReadCursor<'_>) -> [u8; 4] { + match self { + ColorStrideReader::Color { + bpp, + read_stide_bytes, + stride_data_bytes, + stride_padding, + } => { + if read_stide_bytes == stride_data_bytes { + *read_stide_bytes = 0; + cursor.read_slice(*stride_padding); + } + + match bpp { + 16 => { + *read_stide_bytes += 2; + let color_16bit = cursor.read_u16(); + let [r, g, b] = rdp_16bit_to_rgb(color_16bit); + [r, g, b, 0xff] + } + 24 => { + *read_stide_bytes += 3; + + let color_24bit = cursor.read_array::<3>(); + [color_24bit[2], color_24bit[1], color_24bit[0], 0xff] + } + 32 => { + *read_stide_bytes += 4; + let color_32bit = cursor.read_array::<4>(); + [color_32bit[2], color_32bit[1], color_32bit[0], color_32bit[3]] + } + _ => unreachable!("per the invariant on self.bpp, this path is unreachable"), + } + } + ColorStrideReader::Bitmask(bitask) => { + if bitask.next_bit(cursor) == 1 { + [0xff, 0xff, 0xff, 0xff] + } else { + [0, 0, 0, 0xff] + } + } + } + } +} + +fn bit_stride_size_align_u8(size_bits: usize) -> usize { + size_bits.div_ceil(8) +} + +fn bit_stride_size_align_u16(size_bits: usize) -> usize { + size_bits.div_ceil(16) * 2 +} + +/// Message-agnostic pointer data. +struct PointerData<'a> { + width: u16, + height: u16, + xor_bpp: u16, + xor_mask: &'a [u8], + and_mask: &'a [u8], + hot_spot_x: u16, + hot_spot_y: u16, +} diff --git a/crates/ironrdp-graphics/src/quantization.rs b/crates/ironrdp-graphics/src/quantization.rs new file mode 100644 index 00000000..c4ab7d97 --- /dev/null +++ b/crates/ironrdp-graphics/src/quantization.rs @@ -0,0 +1,417 @@ +use ironrdp_pdu::codecs::rfx::Quant; + +const FIRST_LEVEL_SIZE: usize = 1024; +const SECOND_LEVEL_SIZE: usize = 256; +const THIRD_LEVEL_SIZE: usize = 64; + +const FIRST_LEVEL_SUBBANDS_COUNT: usize = 3; +const SECOND_LEVEL_SUBBANDS_COUNT: usize = 3; + +pub fn decode(buffer: &mut [i16], quant: &Quant) { + let (first_level, buffer) = buffer.split_at_mut(FIRST_LEVEL_SUBBANDS_COUNT * FIRST_LEVEL_SIZE); + let (second_level, third_level) = buffer.split_at_mut(SECOND_LEVEL_SUBBANDS_COUNT * SECOND_LEVEL_SIZE); + + let decode_chunk = |a: (&mut [i16], u8)| decode_block(a.0, i16::from(a.1) - 1); + + first_level + .chunks_mut(FIRST_LEVEL_SIZE) + .zip([quant.hl1, quant.lh1, quant.hh1].iter().copied()) + .for_each(decode_chunk); + + second_level + .chunks_mut(SECOND_LEVEL_SIZE) + .zip([quant.hl2, quant.lh2, quant.hh2].iter().copied()) + .for_each(decode_chunk); + + third_level + .chunks_mut(THIRD_LEVEL_SIZE) + .zip([quant.hl3, quant.lh3, quant.hh3, quant.ll3].iter().copied()) + .for_each(decode_chunk); +} + +fn decode_block(buffer: &mut [i16], factor: i16) { + if factor > 0 { + for value in buffer { + *value <<= factor; + } + } +} + +fn encode_block(buffer: &mut [i16], factor: i16) { + if factor > 0 { + for value in buffer { + *value >>= factor; + } + } +} + +pub fn encode(buffer: &mut [i16], quant: &Quant) { + let (first_level, buffer) = buffer.split_at_mut(FIRST_LEVEL_SUBBANDS_COUNT * FIRST_LEVEL_SIZE); + let (second_level, third_level) = buffer.split_at_mut(SECOND_LEVEL_SUBBANDS_COUNT * SECOND_LEVEL_SIZE); + + let encode_chunk = |a: (&mut [i16], u8)| encode_block(a.0, i16::from(a.1) - 1); + + first_level + .chunks_mut(FIRST_LEVEL_SIZE) + .zip([quant.hl1, quant.lh1, quant.hh1].iter().copied()) + .for_each(encode_chunk); + + second_level + .chunks_mut(SECOND_LEVEL_SIZE) + .zip([quant.hl2, quant.lh2, quant.hh2].iter().copied()) + .for_each(encode_chunk); + + third_level + .chunks_mut(THIRD_LEVEL_SIZE) + .zip([quant.hl3, quant.lh3, quant.hh3, quant.ll3].iter().copied()) + .for_each(encode_chunk); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encode_does_not_change_buffer_with_null_quant_values() { + let mut buffer = DEQUANTIZED_BUFFER; + let expected = DEQUANTIZED_BUFFER; + let quant = Quant { + ll3: 0, + lh3: 0, + hl3: 0, + hh3: 0, + lh2: 0, + hl2: 0, + hh2: 0, + lh1: 0, + hl1: 0, + hh1: 0, + }; + + encode(&mut buffer, &quant); + assert_eq!(expected.as_ref(), buffer.as_ref()); + } + + #[test] + fn encode_works_with_not_empty_quant_values() { + let mut buffer = DEQUANTIZED_BUFFER; + let expected = QUANTIZED_BUFFER.as_ref(); + let quant = Quant { + ll3: 6, + lh3: 6, + hl3: 6, + hh3: 6, + lh2: 7, + hl2: 7, + hh2: 8, + lh1: 8, + hl1: 8, + hh1: 9, + }; + + encode(&mut buffer, &quant); + assert_eq!(expected, buffer.as_ref()); + } + + #[test] + fn decode_does_not_change_buffer_with_null_quant_values() { + let mut buffer = QUANTIZED_BUFFER; + let expected = QUANTIZED_BUFFER; + let quant = Quant { + ll3: 0, + lh3: 0, + hl3: 0, + hh3: 0, + lh2: 0, + hl2: 0, + hh2: 0, + lh1: 0, + hl1: 0, + hh1: 0, + }; + + decode(&mut buffer, &quant); + assert_eq!(expected.as_ref(), buffer.as_ref()); + } + + #[test] + fn decode_works_with_not_empty_quant_values() { + let mut buffer = QUANTIZED_BUFFER; + let expected = DEQUANTIZED_BUFFER.as_ref(); + let quant = Quant { + ll3: 6, + lh3: 6, + hl3: 6, + hh3: 6, + lh2: 7, + hl2: 7, + hh2: 8, + lh1: 8, + hl1: 8, + hh1: 9, + }; + + decode(&mut buffer, &quant); + assert_eq!(expected, buffer.as_ref()); + } + + const QUANTIZED_BUFFER: [i16; 4096] = [ + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, -2, 4, + -5, -1, 0, 0, 0, 0, 0, 0, -1, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 3, 0, 0, 0, 0, 0, 0, -1, 4, 4, -5, -1, + 0, 0, 0, 0, 0, 0, -2, 5, 0, 0, 0, 0, 0, 0, 2, 1, 0, 2, -5, -4, 1, 0, 0, 0, 0, 0, 0, 1, 3, -1, 5, 0, -1, 0, 0, + 0, 0, 0, -2, -2, -3, 0, 0, 0, 0, 0, 3, 0, 0, 7, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 2, -1, 0, 0, 0, 0, 0, + -3, 1, 5, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, -2, + 0, 0, 0, 0, 0, 1, 0, -9, -3, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, + 0, 0, 0, 6, -2, -8, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, + 5, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, -1, 2, 0, 0, 1, 0, 0, -1, 0, -1, 7, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -2, 0, 0, 0, 0, 0, 3, -1, 0, -1, 0, 1, 0, 0, 0, 1, 2, -1, 0, -5, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -3, -4, 0, 0, 0, 0, 0, -1, -6, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 6, 0, -1, 0, 1, 0, 0, 1, -1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 1, 10, 0, 0, 0, -1, 1, 0, 0, 0, 0, -1, 1, + 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -5, -5, 3, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, + 0, -1, -2, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 3, -1, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -6, 0, 0, 0, 0, 0, 0, -8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 2, 4, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 6, -1, 0, 0, 0, 0, -1, 7, -1, 0, 0, 0, + -1, -1, 0, 0, 0, 0, 0, -3, -1, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 4, 1, 2, -2, -2, 0, -1, 0, + 0, 0, 0, 1, -1, -7, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -1, -10, 3, 1, 2, 0, -2, 1, 1, 0, 0, 0, + 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 3, -2, 0, 0, 0, 0, 0, 6, -8, -2, 0, -2, 1, 0, 0, -1, -1, -1, 1, 1, 1, + 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 3, 2, 0, 1, 0, 0, 1, 1, 0, 0, 1, -2, 5, -3, 2, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -3, 0, 1, 0, 0, 1, 1, 0, 0, + 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, -1, 0, 0, 1, 0, 0, 0, 3, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 1, 1, 1, -2, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, -1, 0, 0, -1, -1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, -2, -8, 2, 0, + 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, -2, 4, 4, -1, 0, 0, 0, 0, 0, + 0, -1, 3, -1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 3, 3, -7, -1, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, -2, 2, -1, + 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, -5, -3, -4, -6, -6, 7, 1, 1, -1, 0, 0, 0, 0, 0, + 0, 0, -1, -2, -3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, -1, 0, -2, -3, 1, 1, -8, 6, 0, -1, 1, 0, 0, 0, 0, 1, + 1, 0, 0, 6, -3, -1, 0, 0, 0, 0, 0, -1, 5, 2, -6, -2, 0, 0, 0, 2, 7, -6, -1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + -1, -1, 1, 6, 0, 0, 0, 0, 0, 0, -2, 1, 0, 0, 0, 0, 0, 0, 0, -1, 6, -1, 1, -6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, + -1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, 0, -8, 6, 6, 4, 3, 3, 4, -1, -2, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -1, -1, 7, -8, 0, 1, -2, -4, -3, -1, 1, -3, -4, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, -3, -5, 0, 1, 0, 0, 8, -1, -7, -9, -7, -2, 6, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, -1, -1, 0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, -1, 1, 0, 0, 1, -1, 0, 0, 0, 0, -1, 1, -1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 3, 3, 0, 1, 1, -1, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 1, -4, -5, -8, -6, -4, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 3, -9, -5, 10, 2, 2, 0, -1, 2, 6, -4, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 2, -1, -1, 1, 0, 0, 0, 0, 0, -1, 4, -2, -1, 1, -1, 1, -1, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, -1, 1, 1, 0, 1, 0, -1, 3, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 1, 1, 1, 0, -2, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, + 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 2, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, + 0, 0, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, -5, 3, -2, 1, 0, -1, 0, 0, 0, 0, 1, -4, -1, 0, 0, 1, -5, -2, 1, + 1, 1, 6, -2, -1, 0, 1, -4, 21, 6, 0, 0, -5, -1, 1, 2, 4, 4, 3, 5, 0, 0, -1, 4, -8, -11, 0, 0, -2, 8, -1, 0, 3, + 1, -18, 16, 1, -1, 2, 0, -4, -4, 0, 0, -6, 1, -3, -1, 1, 14, -23, 1, 0, 1, 0, 1, -3, -1, 0, 0, 0, -5, -2, 3, + -1, 11, -8, -3, 0, 1, 0, -1, -3, 1, 0, 14, 1, 0, -1, 0, 0, -15, -2, 7, 1, -1, 1, -3, -1, -1, -2, -14, 0, 1, 0, + 0, 5, -18, 14, -7, -1, -1, -2, 13, 1, -1, 3, -14, 1, -2, 2, -1, 11, -16, -2, 2, -1, 1, 0, -9, 0, -1, 19, 1, 1, + 1, 0, 2, 2, 8, 0, 0, 0, 0, 1, -13, 0, 0, -6, -3, -1, 0, -2, -2, 6, 13, 0, 0, 0, 1, 9, 1, 0, 0, -1, 2, 0, 0, 1, + 0, -5, 0, 0, -1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, -4, 1, 4, 4, -2, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, -4, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -1, 1, 1, -1, -3, 0, -2, -3, 0, + 0, 0, 0, 0, 0, 0, -2, -2, 0, 1, -1, 2, 0, 0, 0, 1, -1, 1, 0, 1, -8, -1, 0, 0, 1, -3, 0, 0, -1, 3, 0, 0, 1, -3, + -3, -3, 3, -4, 0, 0, 0, 1, 0, 0, 3, -3, 3, -1, 1, 1, 0, 1, -5, 20, 0, 0, 0, -1, 2, 7, -1, 0, 3, -2, 1, 0, 4, + -1, 0, 1, 0, 1, -3, 2, -9, -12, -19, 10, -1, 2, 0, -1, 1, 9, -13, 0, 0, 1, -4, -13, 0, 0, 16, -7, 0, 16, -3, 1, + -2, -1, 2, -1, 0, 0, 0, 1, 1, 0, 0, 4, -3, -17, -14, 2, -4, -14, -1, 0, -1, 1, 0, -1, 0, 1, 1, -2, 1, 3, 17, + 13, 15, 9, -2, 0, 2, -3, 2, 2, -2, 2, 1, -1, 1, 0, 0, -2, 5, 7, -1, 0, 1, 4, -7, -14, -7, 2, -2, 0, 0, 0, 0, 0, + 0, -1, 1, 0, -3, 17, 8, -1, 4, 8, -9, 6, 0, 1, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, -2, -2, 2, 0, 0, 5, -1, + 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -5, -4, -2, -6, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -1, 0, -4, -1, 0, 0, 0, 0, 0, 0, -1, -1, 1, -1, 0, 0, 0, 0, 2, 2, 0, 0, 0, 3, 0, 0, 1, 1, -1, + 1, 0, 0, 0, 0, -4, -8, 0, 0, 0, -1, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, -1, -3, 3, 0, 0, 0, + -1, 1, 0, 0, 0, 0, 1, -2, 0, 0, -1, 3, 1, 2, -1, 0, 0, 0, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, -3, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 2, 0, -1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, -2, 0, 0, 2, + 2, 1, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 2, 0, 0, 1, -2, 1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, -1, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -2, -2, 1, 0, -1, -2, 0, -1, 8, 6, 3, 0, -1, -6, 0, 4, 5, -13, -53, -3, -2, 0, + -2, -15, -7, 25, 8, 3, 7, -25, 26, -12, 5, 20, 6, 7, -5, -13, 2, -2, 5, -54, 10, -2, 30, 3, -46, 11, -9, -1, + -2, 1, -18, -2, 8, -3, 3, -5, -4, -1, -5, -2, -3, 0, 1, -10, -3, 1, -1, 1, -5, -1, -3, 3, 7, -14, -13, 10, 0, + -1, 11, 2, 5, 2, 12, 0, -8, 24, -21, -49, -4, 10, 9, 31, -1, 5, 10, 1, 13, -57, -52, 9, 12, -6, -18, 5, 1, -3, + -4, -1, 13, 21, 8, 11, 3, 7, 7, -2, -3, -1, 0, 2, -14, -19, -7, 1, -1, -1, -3, -2, 0, 0, -1, -1, -1, -7, 3, 4, + 7, -1, -6, -14, -1, -7, 3, -12, -16, -2, 2, -1, -9, 1, 7, -21, 25, -4, -2, -23, 2, 2, 3, -4, 22, -4, 5, -4, -2, + -2, 4, -7, 0, 0, 3, 2, 17, -6, 7, 5, 3, 1, -5, 0, 1, 0, 0, 6, 1, -4, 2, 0, -1, 0, -1, -15, -12, -12, -19, -23, + 27, 14, 3, 37, 11, -12, -15, -16, 29, 25, 46, 103, 86, 43, 26, 11, 38, 31, 56, 86, 63, 128, 115, 27, 72, -31, + 54, 125, 9, 97, 67, 10, 67, -34, 60, 85, 102, 107, 113, 6, 21, -10, 10, -16, 126, 108, 85, -10, 59, 52, 39, 30, + 35, 53, 15, -3, + ]; + + const DEQUANTIZED_BUFFER: [i16; 4096] = [ + 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 256, 0, 0, 0, 0, -128, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, + -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, 0, 0, 0, 0, 0, 0, 0, 0, -384, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 256, + 0, 0, 0, 0, 0, 0, 0, 128, 0, -256, 512, -640, -128, 0, 0, 0, 0, 0, 0, -128, -384, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -384, 0, 384, 0, 0, 0, 0, 0, 0, -128, 512, 512, -640, -128, 0, 0, 0, 0, 0, 0, -256, 640, 0, 0, 0, 0, 0, 0, 256, + 128, 0, 256, -640, -512, 128, 0, 0, 0, 0, 0, 0, 128, 384, -128, 640, 0, -128, 0, 0, 0, 0, 0, -256, -256, -384, + 0, 0, 0, 0, 0, 384, 0, 0, 896, 0, -512, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, -128, 256, -128, 0, 0, 0, 0, 0, -384, + 128, 640, 0, 0, 0, 0, 0, 0, -128, 0, 0, 128, 896, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 256, 0, + 0, 0, -256, 0, 0, 0, 0, 0, 128, 0, -1152, -384, 640, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, + 0, -128, -128, -128, 0, 0, 0, 0, 0, 0, 768, -256, -1024, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, + 0, 0, -384, 0, 0, 0, 0, 0, 0, -128, 0, -128, 0, 640, 256, 384, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 256, 0, 0, 0, 0, + 0, 0, -128, 128, 0, 0, -128, 256, 0, 0, 128, 0, 0, -128, 0, -128, 896, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -384, + -256, 0, 0, 0, 0, 0, 384, -128, 0, -128, 0, 128, 0, 0, 0, 128, 256, -128, 0, -640, 128, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -384, -512, 0, 0, 0, 0, 0, -128, -768, 128, 0, -128, 0, 0, 0, 0, 0, 0, 0, 768, 0, -128, 0, 128, 0, 0, 128, + -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 384, -640, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, -128, 128, 1280, 0, 0, 0, + -128, 128, 0, 0, 0, 0, -128, 128, 0, 0, 0, 0, 0, 0, 896, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, -640, -640, 384, 0, + 0, 0, 0, 0, 0, 0, 0, -128, 0, -128, 0, 0, 0, 0, 0, -128, -256, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 384, -128, + -896, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, -768, 0, 0, 0, 0, 0, 0, -1024, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 384, 256, + 512, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 768, -128, 0, 0, 0, 0, -128, 896, -128, 0, 0, 0, -128, -128, 0, 0, 0, 0, + 0, -384, -128, 768, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 384, 128, 0, 0, 0, 0, 0, 512, 128, 256, -256, -256, 0, -128, + 0, 0, 0, 0, 128, -128, -896, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -768, 0, 0, 0, 0, 0, -128, -1280, 384, 128, + 256, 0, -256, 128, 128, 0, 0, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 384, -256, 0, 0, 0, 0, 0, 768, + -1024, -256, 0, -256, 128, 0, 0, -128, -128, -128, 128, 128, 128, 768, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 512, 128, + 0, 0, 0, 0, 0, 384, 256, 0, 128, 0, 0, 128, 128, 0, 0, 128, -256, 640, -384, 256, 0, 0, 0, 0, 128, 0, 0, 0, 0, + 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, -384, 0, 128, 0, 0, 128, 128, 0, 0, 0, 0, + 0, -512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, -128, -128, 0, 0, 128, 0, 0, 0, + 384, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 128, 128, 128, -256, 0, 128, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, -256, 128, 128, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 384, -128, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, + -128, -128, 0, 0, -128, -128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, -128, -128, 0, 0, 0, 0, + -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, -128, -128, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, 0, 0, 0, 0, 0, 0, -256, -1024, 256, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 128, 0, -256, 512, 512, -128, 0, 0, 0, 0, 0, 0, -128, 384, -128, + 0, 0, 0, 0, 0, 0, 128, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, -128, 384, 384, -896, -128, 0, 0, 0, 0, 0, 0, 0, 0, + 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 768, 384, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 128, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1024, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 128, -256, -256, 256, -128, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, + 128, 128, 384, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, -128, + -640, -384, -512, -768, -768, 896, 128, 128, -128, 0, 0, 0, 0, 0, 0, 0, -128, -256, -384, 0, 128, 0, 0, 0, 0, + 0, 0, 0, 0, 128, 0, 256, -128, 0, -256, -384, 128, 128, -1024, 768, 0, -128, 128, 0, 0, 0, 0, 128, 128, 0, 0, + 768, -384, -128, 0, 0, 0, 0, 0, -128, 640, 256, -768, -256, 0, 0, 0, 256, 896, -768, -128, 0, 0, 128, 0, 0, 0, + 0, 0, 0, 0, 0, -128, -128, 128, 768, 0, 0, 0, 0, 0, 0, -256, 128, 0, 0, 0, 0, 0, 0, 0, -128, 768, -128, 128, + -768, 896, 0, 0, 0, 0, 0, 0, 0, 0, 128, -128, -128, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -128, -256, 0, -1024, 768, 768, 512, 384, 384, 512, -128, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 256, -128, -128, 896, -1024, 0, 128, -256, -512, -384, -128, 128, -384, -512, 128, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 128, 0, 128, -384, -640, 0, 128, 0, 0, 1024, -128, -896, -1152, -896, -256, 768, 384, 0, 0, + 0, 0, 0, 0, 0, 128, 0, 0, 0, -128, -128, 0, 0, 0, 0, 0, 128, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, 0, 0, 0, 0, 0, -128, 128, 0, 0, 128, -128, 0, + 0, 0, 0, -128, 128, -128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, -128, + 0, 0, 0, 0, 0, 0, 128, 128, 384, 384, 0, 128, 128, -128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 128, 0, 0, 0, 128, 0, 0, 0, 256, 128, -512, -640, -1024, -768, -512, 128, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 384, -1152, -640, 1280, 256, 256, 0, -128, 256, 768, -512, -128, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 256, -128, -128, 128, 0, 0, 0, 0, 0, -128, 512, -256, + -128, 128, -128, 128, -128, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 128, -128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -128, -128, 0, -128, 128, 128, 0, 128, 0, -128, 384, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -128, -128, -128, 128, 128, 128, 0, -256, -384, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 512, 256, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -512, -256, 256, 0, 0, 0, 0, 0, 0, 0, -256, -256, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, -256, 256, -512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 256, -256, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 256, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 256, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, + 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -256, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 256, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, -256, 512, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, -256, 0, 0, 0, 0, 0, 0, 0, 256, -256, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, -256, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 64, 0, 0, 64, 0, 0, 64, 0, 0, 0, 0, 0, 0, -64, 0, 0, -64, -64, 0, -320, 192, -128, 64, 0, -64, 0, 0, 0, 0, + 64, -256, -64, 0, 0, 64, -320, -128, 64, 64, 64, 384, -128, -64, 0, 64, -256, 1344, 384, 0, 0, -320, -64, 64, + 128, 256, 256, 192, 320, 0, 0, -64, 256, -512, -704, 0, 0, -128, 512, -64, 0, 192, 64, -1152, 1024, 64, -64, + 128, 0, -256, -256, 0, 0, -384, 64, -192, -64, 64, 896, -1472, 64, 0, 64, 0, 64, -192, -64, 0, 0, 0, -320, + -128, 192, -64, 704, -512, -192, 0, 64, 0, -64, -192, 64, 0, 896, 64, 0, -64, 0, 0, -960, -128, 448, 64, -64, + 64, -192, -64, -64, -128, -896, 0, 64, 0, 0, 320, -1152, 896, -448, -64, -64, -128, 832, 64, -64, 192, -896, + 64, -128, 128, -64, 704, -1024, -128, 128, -64, 64, 0, -576, 0, -64, 1216, 64, 64, 64, 0, 128, 128, 512, 0, 0, + 0, 0, 64, -832, 0, 0, -384, -192, -64, 0, -128, -128, 384, 832, 0, 0, 0, 64, 576, 64, 0, 0, -64, 128, 0, 0, 64, + 0, -320, 0, 0, -64, 0, 0, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 64, -128, -256, 64, 256, 256, -128, 0, 0, -64, 0, 0, + 0, 0, 0, 0, 0, 0, 64, 0, -256, -64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, -64, 64, + 64, -64, -192, 0, -128, -192, 0, 0, 0, 0, 0, 0, 0, -128, -128, 0, 64, -64, 128, 0, 0, 0, 64, -64, 64, 0, 64, + -512, -64, 0, 0, 64, -192, 0, 0, -64, 192, 0, 0, 64, -192, -192, -192, 192, -256, 0, 0, 0, 64, 0, 0, 192, -192, + 192, -64, 64, 64, 0, 64, -320, 1280, 0, 0, 0, -64, 128, 448, -64, 0, 192, -128, 64, 0, 256, -64, 0, 64, 0, 64, + -192, 128, -576, -768, -1216, 640, -64, 128, 0, -64, 64, 576, -832, 0, 0, 64, -256, -832, 0, 0, 1024, -448, 0, + 1024, -192, 64, -128, -64, 128, -64, 0, 0, 0, 64, 64, 0, 0, 256, -192, -1088, -896, 128, -256, -896, -64, 0, + -64, 64, 0, -64, 0, 64, 64, -128, 64, 192, 1088, 832, 960, 576, -128, 0, 128, -192, 128, 128, -128, 128, 64, + -64, 64, 0, 0, -128, 320, 448, -64, 0, 64, 256, -448, -896, -448, 128, -128, 0, 0, 0, 0, 0, 0, -64, 64, 0, + -192, 1088, 512, -64, 256, 512, -576, 384, 0, 64, 0, 0, 0, 0, 0, 0, -64, 64, 0, 0, 0, 0, 0, -128, -128, 128, 0, + 0, 320, -64, 0, 0, -64, 0, 0, 0, 0, 0, 0, 0, -320, -256, -128, -384, -64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -448, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, -512, -128, 0, 0, 0, 0, 0, 0, -128, -128, 128, -128, + 0, 0, 0, 0, 256, 256, 0, 0, 0, 384, 0, 0, 128, 128, -128, 128, 0, 0, 0, 0, -512, -1024, 0, 0, 0, -128, 0, 0, + -128, -128, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 128, -128, -384, 384, 0, 0, 0, -128, 128, 0, 0, 0, 0, 128, + -256, 0, 0, -128, 384, 128, 256, -128, 0, 0, 0, -256, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 128, 128, 0, + -384, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, -128, 256, 0, -128, 128, 0, 0, 0, 128, 128, 0, 0, 0, 0, 0, -128, 0, 0, 0, + -128, 0, -256, 0, 0, 256, 256, 128, -128, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 256, 0, 0, 128, -256, + 128, -128, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, -128, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 128, 128, -128, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, -64, -64, 32, + 0, -32, -64, 0, -32, 256, 192, 96, 0, -32, -192, 0, 128, 160, -416, -1696, -96, -64, 0, -64, -480, -224, 800, + 256, 96, 224, -800, 832, -384, 160, 640, 192, 224, -160, -416, 64, -64, 160, -1728, 320, -64, 960, 96, -1472, + 352, -288, -32, -64, 32, -576, -64, 256, -96, 96, -160, -128, -32, -160, -64, -96, 0, 32, -320, -96, 32, -32, + 32, -160, -32, -96, 96, 224, -448, -416, 320, 0, -32, 352, 64, 160, 64, 384, 0, -256, 768, -672, -1568, -128, + 320, 288, 992, -32, 160, 320, 32, 416, -1824, -1664, 288, 384, -192, -576, 160, 32, -96, -128, -32, 416, 672, + 256, 352, 96, 224, 224, -64, -96, -32, 0, 64, -448, -608, -224, 32, -32, -32, -96, -64, 0, 0, -32, -32, -32, + -224, 96, 128, 224, -32, -192, -448, -32, -224, 96, -384, -512, -64, 64, -32, -288, 32, 224, -672, 800, -128, + -64, -736, 64, 64, 96, -128, 704, -128, 160, -128, -64, -64, 128, -224, 0, 0, 96, 64, 544, -192, 224, 160, 96, + 32, -160, 0, 32, 0, 0, 192, 32, -128, 64, 0, -32, 0, -32, -480, -384, -384, -608, -736, 864, 448, 96, 1184, + 352, -384, -480, -512, 928, 800, 1472, 3296, 2752, 1376, 832, 352, 1216, 992, 1792, 2752, 2016, 4096, 3680, + 864, 2304, -992, 1728, 4000, 288, 3104, 2144, 320, 2144, -1088, 1920, 2720, 3264, 3424, 3616, 192, 672, -320, + 320, -512, 4032, 3456, 2720, -320, 1888, 1664, 1248, 960, 1120, 1696, 480, -96, + ]; +} diff --git a/crates/ironrdp-graphics/src/rdp6/bitmap_stream/decoder.rs b/crates/ironrdp-graphics/src/rdp6/bitmap_stream/decoder.rs new file mode 100644 index 00000000..d5f07b67 --- /dev/null +++ b/crates/ironrdp-graphics/src/rdp6/bitmap_stream/decoder.rs @@ -0,0 +1,305 @@ +use ironrdp_core::{decode, DecodeError}; +use ironrdp_pdu::bitmap::rdp6::{BitmapStream as BitmapStreamPdu, ColorPlaneDefinition}; + +use crate::color_conversion::Rgb; +use crate::rdp6::rle::{decompress_8bpp_plane, RleDecodeError}; + +#[derive(Debug)] +pub enum BitmapDecodeError { + Decode(DecodeError), + Rle(RleDecodeError), + InvalidUncompressedDataSize, +} + +impl core::fmt::Display for BitmapDecodeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + BitmapDecodeError::Decode(_error) => write!(f, "failed to decode RDP6 bitmap stream PDU"), + BitmapDecodeError::Rle(_error) => { + write!(f, "failed to perform RLE decompression of RDP6 bitmap stream") + } + BitmapDecodeError::InvalidUncompressedDataSize => write!( + f, + "color plane data size provided in PDU is not sufficient to reconstruct the bitmap" + ), + } + } +} + +impl core::error::Error for BitmapDecodeError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + BitmapDecodeError::Decode(err) => Some(err), + BitmapDecodeError::Rle(err) => Some(err), + BitmapDecodeError::InvalidUncompressedDataSize => None, + } + } +} + +impl From for BitmapDecodeError { + fn from(err: DecodeError) -> Self { + BitmapDecodeError::Decode(err) + } +} + +impl From for BitmapDecodeError { + fn from(err: RleDecodeError) -> Self { + BitmapDecodeError::Rle(err) + } +} + +/// Implements decoding of RDP6 bitmap stream PDU (see [`BitmapStreamPdu`]) +#[derive(Debug, Default)] +pub struct BitmapStreamDecoder { + /// Optimization to avoid reallocations, re-use this buffer for all bitmaps in the session + planes_buffer: Vec, +} + +/// Internal implementation of RDP6 bitmap stream PDU decoder for specific image size and format +struct BitmapStreamDecoderImpl<'a> { + bitmap: BitmapStreamPdu<'a>, + image_width: usize, + image_height: usize, + chroma_width: usize, + chroma_height: usize, + full_plane_size: usize, + chroma_plane_size: usize, + uncompressed_planes_size: usize, + color_plane_offsets: [usize; 3], +} + +struct AYCoCgParams { + color_loss_level: u8, + chroma_subsampling: bool, + alpha: bool, +} + +impl<'a> BitmapStreamDecoderImpl<'a> { + fn init(bitmap: BitmapStreamPdu<'a>, image_width: usize, image_height: usize) -> Self { + let (chroma_width, chroma_height) = if bitmap.has_subsampled_chroma() { + // When image is subsampled, chroma plane has half the size of the luma plane, however + // its size is rounded up to the nearest greater integer, to take into account odd image + // size (e.g. if width is 3, then chroma plane width is 2, not 1, to take into account + // the odd column which expands to 1 pixel instead of 2 during supersampling) + (image_width.div_ceil(2), image_height.div_ceil(2)) + } else { + (image_width, image_height) + }; + + let full_plane_size = image_width * image_height; + let chroma_plane_size = chroma_width * chroma_height; + + let uncompressed_planes_size = if bitmap.has_subsampled_chroma() { + full_plane_size + chroma_plane_size * 2 + } else { + full_plane_size * 3 + }; + + let color_plane_offsets = [0, full_plane_size, full_plane_size + chroma_plane_size]; + + Self { + bitmap, + image_width, + image_height, + chroma_width, + chroma_height, + full_plane_size, + chroma_plane_size, + uncompressed_planes_size, + color_plane_offsets, + } + } + + fn decompress_planes(&'a self, aux_buffer: &'a mut Vec) -> Result<&'a [u8], BitmapDecodeError> { + let planes = if self.bitmap.header.enable_rle_compression { + // We don't care for the previous content, just resize it to fit the data + aux_buffer.resize(self.uncompressed_planes_size, 0); + let uncompressed_planes_buffer = &mut aux_buffer[..self.uncompressed_planes_size]; + + let compressed = self.bitmap.color_panes_data(); + let mut src_offset = 0; + + // Decompress Alpha plane + if self.bitmap.header.use_alpha { + // Decompress alpha alpha, but discard it (always 0xFF) + src_offset += decompress_8bpp_plane( + &compressed[src_offset..], + uncompressed_planes_buffer, + self.image_width, + self.image_height, + )?; + } + + // Decompress R/Y plane + src_offset += decompress_8bpp_plane( + &compressed[src_offset..], + &mut uncompressed_planes_buffer[self.color_plane_offsets[0]..], + self.image_width, + self.image_height, + )?; + + // Decompress G/Co plane + src_offset += decompress_8bpp_plane( + &compressed[src_offset..], + &mut uncompressed_planes_buffer[self.color_plane_offsets[1]..], + self.chroma_width, + self.chroma_height, + )?; + + // Decompress B/Cg plane + decompress_8bpp_plane( + &compressed[src_offset..], + &mut uncompressed_planes_buffer[self.color_plane_offsets[2]..], + self.chroma_width, + self.chroma_height, + )?; + + &uncompressed_planes_buffer[..self.uncompressed_planes_size] + } else { + // Discard alpha plane + let color_planes_offset = if self.bitmap.header.use_alpha { + self.full_plane_size + } else { + 0 + }; + + let expected_data_size = color_planes_offset + self.uncompressed_planes_size; + + if self.bitmap.color_panes_data().len() < expected_data_size { + return Err(BitmapDecodeError::InvalidUncompressedDataSize); + } + + &self.bitmap.color_panes_data()[color_planes_offset..] + }; + + Ok(planes) + } + + fn write_argb_planes_to_rgb24(&self, planes: &[u8], dst: &mut Vec) { + // For ARGB conversion is simple - just copy data in correct order + let (r_offset, g_offset, b_offset) = ( + self.color_plane_offsets[0], + self.color_plane_offsets[1], + self.color_plane_offsets[2], + ); + + let r_plane = &planes[r_offset..r_offset + self.full_plane_size]; + let g_plane = &planes[g_offset..g_offset + self.full_plane_size]; + let b_plane = &planes[b_offset..b_offset + self.full_plane_size]; + + for i in 0..self.full_plane_size { + let (r, g, b) = (r_plane[i], g_plane[i], b_plane[i]); + + dst.extend_from_slice(&[r, g, b]); + } + } + + fn write_aycocg_planes_to_rgb24(&self, params: AYCoCgParams, planes: &[u8], dst: &mut Vec) { + #![allow(clippy::similar_names, reason = "it’s hard to find better names for co, cg, etc")] + + let sample_shift = usize::from(params.chroma_subsampling); + + let (y_offset, co_offset, cg_offset) = ( + self.color_plane_offsets[0], + self.color_plane_offsets[1], + self.color_plane_offsets[2], + ); + + let y_plane = &planes[y_offset..y_offset + self.full_plane_size]; + let co_plane = &planes[co_offset..co_offset + self.chroma_plane_size]; + let cg_plane = &planes[cg_offset..cg_offset + self.chroma_plane_size]; + + for (idx, y) in y_plane.iter().copied().enumerate() { + let chroma_row = (idx / self.image_width) >> sample_shift; + let chroma_col = (idx % self.image_width) >> sample_shift; + let chroma_idx = chroma_row * self.chroma_width + chroma_col; + + let co = co_plane[chroma_idx]; + let cg = cg_plane[chroma_idx]; + + let Rgb { r, g, b } = ycocg_with_cll_to_rgb(params.color_loss_level, y, co, cg); + + // As described in 3.1.9.1.2 [MS-RDPEGDI], R and B channels are swapped for + // AYCoCg when 24-bit image is used (no alpha). We swap them back here + if params.alpha { + dst.extend_from_slice(&[r, g, b]); + } else { + dst.extend_from_slice(&[b, g, r]); + } + } + } + + fn decode(self, dst: &mut Vec, aux_buffer: &'a mut Vec) -> Result<(), BitmapDecodeError> { + // Reserve enough space for decoded RGB channels data + dst.reserve(self.image_height * self.image_width * 3); + + match self.bitmap.header.color_plane_definition { + ColorPlaneDefinition::Argb => { + let color_planes = self.decompress_planes(aux_buffer)?; + self.write_argb_planes_to_rgb24(color_planes, dst); + } + ColorPlaneDefinition::AYCoCg { + color_loss_level, + use_chroma_subsampling, + .. + } => { + let params: AYCoCgParams = AYCoCgParams { + color_loss_level, + chroma_subsampling: use_chroma_subsampling, + alpha: self.bitmap.header.use_alpha, + }; + let color_planes = self.decompress_planes(aux_buffer)?; + self.write_aycocg_planes_to_rgb24(params, color_planes, dst); + } + } + + Ok(()) + } +} + +/// Perform YCoCg -> RGB conversion with color loss reduction (CLL) correction. +fn ycocg_with_cll_to_rgb(cll: u8, y: u8, co: u8, cg: u8) -> Rgb { + #![allow(clippy::similar_names)] // It’s hard to find better names for co, cg, etc. + + // We decrease CLL by 1 to skip division by 2 for co & cg components during computation of + // the following color conversion matrix: + // |R| |1 1/2 -1/2| |Y | + // |G| = |1 0 1/2| * |Co| + // |B| |1 -1/2 -1/2| |Cg| + let chroma_shift = cll - 1; + + let clip_i16 = + |v: i16| u8::try_from(v.clamp(0, 255)).expect("fits into u8 because the value is clamped to [0..256]"); + + let co_signed = (co << chroma_shift).cast_signed(); + let cg_signed = (cg << chroma_shift).cast_signed(); + + let y = i16::from(y); + let co = i16::from(co_signed); + let cg = i16::from(cg_signed); + + let t = y - cg; + let r = clip_i16(t + co); + let g = clip_i16(y + cg); + let b = clip_i16(t - co); + + Rgb { r, g, b } +} + +impl BitmapStreamDecoder { + /// Performs decoding of bitmap stream PDU from `bitmap_data` and writes decoded rgb24 + /// image to `dst` buffer. + pub fn decode_bitmap_stream_to_rgb24( + &mut self, + bitmap_data: &[u8], + dst: &mut Vec, + image_width: usize, + image_height: usize, + ) -> Result<(), BitmapDecodeError> { + let bitmap = decode::>(bitmap_data)?; + + let decoder = BitmapStreamDecoderImpl::init(bitmap, image_width, image_height); + + decoder.decode(dst, &mut self.planes_buffer) + } +} diff --git a/crates/ironrdp-graphics/src/rdp6/bitmap_stream/encoder.rs b/crates/ironrdp-graphics/src/rdp6/bitmap_stream/encoder.rs new file mode 100644 index 00000000..0cf4de28 --- /dev/null +++ b/crates/ironrdp-graphics/src/rdp6/bitmap_stream/encoder.rs @@ -0,0 +1,289 @@ +use ironrdp_core::{not_enough_bytes_err, EncodeError, WriteCursor}; +use ironrdp_pdu::bitmap::rdp6::{BitmapStreamHeader, ColorPlaneDefinition}; + +use crate::rdp6::rle::{compress_8bpp_plane, RleEncodeError}; + +#[derive(Debug)] +pub enum BitmapEncodeError { + Rle(RleEncodeError), + Encode(EncodeError), +} + +impl core::fmt::Display for BitmapEncodeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + BitmapEncodeError::Rle(_error) => write!(f, "failed to rle compress"), + BitmapEncodeError::Encode(_error) => write!(f, "failed to encode pdu"), + } + } +} + +impl core::error::Error for BitmapEncodeError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + BitmapEncodeError::Rle(error) => Some(error), + BitmapEncodeError::Encode(error) => Some(error), + } + } +} + +pub trait ColorChannels { + const STRIDE: usize; + const R: usize; + const G: usize; + const B: usize; +} + +pub trait AlphaChannel { + const A: usize; +} + +pub trait PixelFormat { + const STRIDE: usize; + + fn r(pixel: &[u8]) -> u8; + fn g(pixel: &[u8]) -> u8; + fn b(pixel: &[u8]) -> u8; +} + +pub trait PixelAlpha: PixelFormat { + fn a(pixel: &[u8]) -> u8; +} + +impl PixelFormat for T +where + T: ColorChannels, +{ + const STRIDE: usize = T::STRIDE; + + fn r(pixel: &[u8]) -> u8 { + pixel[T::R] + } + + fn g(pixel: &[u8]) -> u8 { + pixel[T::G] + } + + fn b(pixel: &[u8]) -> u8 { + pixel[T::B] + } +} + +impl PixelAlpha for T +where + T: ColorChannels + AlphaChannel, +{ + fn a(pixel: &[u8]) -> u8 { + pixel[T::A] + } +} + +pub struct RgbChannels; + +impl ColorChannels for RgbChannels { + const STRIDE: usize = 3; + const R: usize = 0; + const G: usize = 1; + const B: usize = 2; +} + +pub struct ARgbChannels; + +impl ColorChannels for ARgbChannels { + const STRIDE: usize = 4; + const R: usize = 1; + const G: usize = 2; + const B: usize = 3; +} + +impl AlphaChannel for ARgbChannels { + const A: usize = 0; +} + +pub struct RgbAChannels; + +impl ColorChannels for RgbAChannels { + const STRIDE: usize = 4; + const R: usize = 0; + const G: usize = 1; + const B: usize = 2; +} + +impl AlphaChannel for RgbAChannels { + const A: usize = 3; +} + +pub struct ABgrChannels; + +impl ColorChannels for ABgrChannels { + const STRIDE: usize = 4; + const R: usize = 3; + const G: usize = 2; + const B: usize = 1; +} + +impl AlphaChannel for ABgrChannels { + const A: usize = 0; +} + +pub struct BgrAChannels; + +impl ColorChannels for BgrAChannels { + const STRIDE: usize = 4; + const R: usize = 2; + const G: usize = 1; + const B: usize = 0; +} + +impl AlphaChannel for BgrAChannels { + const A: usize = 3; +} + +impl BitmapEncodeError { + fn rle(e: RleEncodeError) -> Self { + Self::Rle(e) + } +} + +pub struct BitmapStreamEncoder { + width: usize, + height: usize, +} + +impl BitmapStreamEncoder { + pub fn new(width: usize, height: usize) -> Self { + Self { width, height } + } + + pub fn encode_channels_stream( + &mut self, + (r, g, b): (R, G, B), + dst: &mut [u8], + rle: bool, + ) -> Result + where + R: Iterator, + G: Iterator, + B: Iterator, + { + let mut cursor = WriteCursor::new(dst); + + let header = BitmapStreamHeader { + enable_rle_compression: rle, + use_alpha: false, + color_plane_definition: ColorPlaneDefinition::Argb, + }; + + ironrdp_core::encode_cursor(&header, &mut cursor).map_err(BitmapEncodeError::Encode)?; + + if rle { + compress_8bpp_plane(r, &mut cursor, self.width, self.height).map_err(BitmapEncodeError::Rle)?; + compress_8bpp_plane(g, &mut cursor, self.width, self.height).map_err(BitmapEncodeError::Rle)?; + compress_8bpp_plane(b, &mut cursor, self.width, self.height).map_err(BitmapEncodeError::Rle)?; + } else { + let remaining = cursor.len(); + let needed = self.width * self.height * 3 + 1; + if needed > remaining { + return Err(BitmapEncodeError::Encode(not_enough_bytes_err( + "BitmapStreamData", + remaining, + needed, + ))); + } + + for byte in r.chain(g).chain(b) { + cursor.write_u8(byte); + } + cursor.write_u8(0u8); + } + + Ok(cursor.pos()) + } + + pub fn encode_pixels_stream<'a, I, F>( + &mut self, + data: I, + dst: &mut [u8], + rle: bool, + ) -> Result + where + F: PixelFormat, + I: Iterator + Clone, + { + let r = data.clone().map(F::r); + let g = data.clone().map(F::g); + let b = data.map(F::b); + + self.encode_channels_stream((r, g, b), dst, rle) + } + + pub fn encode_bitmap(&mut self, src: &[u8], dst: &mut [u8], rle: bool) -> Result + where + F: PixelFormat, + { + let r = src.chunks_exact(F::STRIDE).map(F::r); + let g = src.chunks_exact(F::STRIDE).map(F::g); + let b = src.chunks_exact(F::STRIDE).map(F::b); + + self.encode_channels_stream((r, g, b), dst, rle) + } + + pub fn encode_channels_stream_alpha( + &mut self, + (r, g, b, a): (R, G, B, A), + dst: &mut [u8], + rle: bool, + ) -> Result + where + R: Iterator, + G: Iterator, + B: Iterator, + A: Iterator, + { + let mut cursor = WriteCursor::new(dst); + + let header = BitmapStreamHeader { + enable_rle_compression: rle, + use_alpha: false, + color_plane_definition: ColorPlaneDefinition::Argb, + }; + + ironrdp_core::encode_cursor(&header, &mut cursor).map_err(BitmapEncodeError::Encode)?; + + if rle { + compress_8bpp_plane(a, &mut cursor, self.width, self.height).map_err(BitmapEncodeError::rle)?; + compress_8bpp_plane(r, &mut cursor, self.width, self.height).map_err(BitmapEncodeError::rle)?; + compress_8bpp_plane(g, &mut cursor, self.width, self.height).map_err(BitmapEncodeError::rle)?; + compress_8bpp_plane(b, &mut cursor, self.width, self.height).map_err(BitmapEncodeError::rle)?; + } else { + let remaining = cursor.len(); + let needed = self.width * self.height * 4 + 1; + if needed > remaining { + return Err(BitmapEncodeError::Encode(not_enough_bytes_err( + "BitmapStreamData", + remaining, + needed, + ))); + } + + for byte in a.chain(r).chain(g).chain(b) { + cursor.write_u8(byte); + } + cursor.write_u8(0u8); + } + + Ok(cursor.pos()) + } + + pub fn encode_bitmap_alpha(&mut self, src: &[u8], dst: &mut [u8], rle: bool) -> Result + where + F: PixelFormat + PixelAlpha, + { + let r = src.chunks_exact(F::STRIDE).map(F::r); + let g = src.chunks_exact(F::STRIDE).map(F::g); + let b = src.chunks_exact(F::STRIDE).map(F::b); + let a = src.chunks_exact(F::STRIDE).map(F::a); + + self.encode_channels_stream_alpha((r, g, b, a), dst, rle) + } +} diff --git a/crates/ironrdp-graphics/src/rdp6/bitmap_stream/mod.rs b/crates/ironrdp-graphics/src/rdp6/bitmap_stream/mod.rs new file mode 100644 index 00000000..90f0f54c --- /dev/null +++ b/crates/ironrdp-graphics/src/rdp6/bitmap_stream/mod.rs @@ -0,0 +1,179 @@ +mod decoder; +mod encoder; + +pub use decoder::*; +pub use encoder::*; + +#[cfg(test)] +mod tests { + use super::*; + + fn buffer_from_bmp(bmp_image: &[u8], width: usize, height: usize) -> Vec { + let expected_bmp = bmp::from_reader(&mut std::io::Cursor::new(bmp_image)).unwrap(); + + let mut expected_buffer = vec![0; width * height * 3]; + for (idx, (x, y)) in expected_bmp.coordinates().enumerate() { + let pixel = expected_bmp.get_pixel(x, y); + + let offset = idx * 3; + expected_buffer[offset] = pixel.r; + expected_buffer[offset + 1] = pixel.g; + expected_buffer[offset + 2] = pixel.b; + } + + expected_buffer + } + + fn assert_decoded_image(pdu: &[u8], expected_bmp: &[u8], width: usize, height: usize) { + let expected_buffer = buffer_from_bmp(expected_bmp, width, height); + + let mut actual = Vec::new(); + BitmapStreamDecoder::default() + .decode_bitmap_stream_to_rgb24(pdu, &mut actual, width, height) + .unwrap(); + + assert_eq!(actual.as_slice(), expected_buffer.as_slice()); + } + + #[test] + fn decode_32x64_rgb_raw() { + // RGB (No alpha), no RLE + assert_decoded_image( + include_bytes!("../test_assets/32x64_rgb_raw.bin"), + include_bytes!("../test_assets/32x64_rgb_raw.bmp"), + 32, + 64, + ); + } + + #[test] + fn decode_64x24_argb_rle() { + // ARGB (With alpha), RLE + assert_decoded_image( + include_bytes!("../test_assets/64x24_argb_rle.bin"), + include_bytes!("../test_assets/64x24_argb_rle.bmp"), + 64, + 24, + ); + } + + #[test] + fn decode_64x64_aycocg_rle() { + // AYCoCg (With alpha), RLE, no chroma subsampling + assert_decoded_image( + include_bytes!("../test_assets/64x64_aycocg_rle.bin"), + include_bytes!("../test_assets/64x64_aycocg_rle.bmp"), + 64, + 64, + ); + } + + #[test] + fn decode_64x64_ycocg_rle_ss() { + // AYCoCg (No alpha), RLE, with chroma subsampling + assert_decoded_image( + include_bytes!("../test_assets/64x64_ycocg_rle_ss.bin"), + include_bytes!("../test_assets/64x64_ycocg_rle_ss.bmp"), + 64, + 64, + ); + } + + #[test] + fn decode_64x35_ycocg_rle_ss() { + // AYCoCg (No alpha), RLE, with chroma subsampling + odd resolution + assert_decoded_image( + include_bytes!("../test_assets/64x35_ycocg_rle_ss.bin"), + include_bytes!("../test_assets/64x35_ycocg_rle_ss.bmp"), + 64, + 35, + ); + } + + #[test] + fn decode_64x64_ycocg_raw_ss() { + // AYCoCg (No alpha), no RLE, with chroma subsampling + assert_decoded_image( + include_bytes!("../test_assets/64x64_ycocg_raw_ss.bin"), + include_bytes!("../test_assets/64x64_ycocg_raw_ss.bmp"), + 64, + 64, + ); + } + + fn assert_encoded_image(expected_pdu: &[u8], bmp: &[u8], width: usize, height: usize, rle: bool) { + let image = buffer_from_bmp(bmp, width, height); + + let mut pdu = vec![0; width * height * 4 + 2]; + let written = BitmapStreamEncoder::new(width, height) + .encode_bitmap::(&image, &mut pdu, rle) + .unwrap(); + + // last byte is padding when !rle + assert_eq!(&pdu[0..written - 1], &expected_pdu[0..written - 1]); + } + + fn encode_decode_test(bmp: &[u8], width: usize, height: usize, rle: bool) { + let image = buffer_from_bmp(bmp, width, height); + + let mut pdu = vec![0; width * height * 4 + 2]; + let written = BitmapStreamEncoder::new(width, height) + .encode_bitmap::(&image, &mut pdu, rle) + .unwrap(); + + let mut actual = Vec::new(); + BitmapStreamDecoder::default() + .decode_bitmap_stream_to_rgb24(&pdu[..written], &mut actual, width, height) + .unwrap(); + + assert_eq!(&image.as_slice(), &actual.as_slice()); + } + + #[test] + fn encode_32x64_rgb_raw() { + // RGB (No alpha), no RLE + assert_encoded_image( + include_bytes!("../test_assets/32x64_rgb_raw.bin"), + include_bytes!("../test_assets/32x64_rgb_raw.bmp"), + 32, + 64, + false, + ); + } + + #[test] + fn encode_decode_32x64_rgb_raw() { + // RGB (No alpha), no RLE + encode_decode_test(include_bytes!("../test_assets/32x64_rgb_raw.bmp"), 32, 64, false); + } + + #[test] + fn encode_decode_32x64_rgb_rle() { + // RGB (No alpha), with RLE + encode_decode_test(include_bytes!("../test_assets/32x64_rgb_raw.bmp"), 32, 64, true); + } + + #[test] + fn encode_decode_64x24_rgb_raw() { + // RGB (No alpha), no RLE + encode_decode_test(include_bytes!("../test_assets/64x24_argb_rle.bmp"), 32, 64, false); + } + + #[test] + fn encode_decode_64x24_rgb_rle() { + // RGB (No alpha), with RLE + encode_decode_test(include_bytes!("../test_assets/64x24_argb_rle.bmp"), 32, 64, true); + } + + #[test] + fn encode_decode_64x64_rgb_raw() { + // RGB (No alpha), no RLE + encode_decode_test(include_bytes!("../test_assets/64x64_aycocg_rle.bmp"), 64, 64, false); + } + + #[test] + fn encode_decode_64x64_rgb_rle() { + // RGB (No alpha), with RLE + encode_decode_test(include_bytes!("../test_assets/64x64_aycocg_rle.bmp"), 64, 64, true); + } +} diff --git a/crates/ironrdp-graphics/src/rdp6/mod.rs b/crates/ironrdp-graphics/src/rdp6/mod.rs new file mode 100644 index 00000000..2379e8e7 --- /dev/null +++ b/crates/ironrdp-graphics/src/rdp6/mod.rs @@ -0,0 +1,7 @@ +//! This module provides the RDP6 bitmap decoder implementation + +pub(crate) mod bitmap_stream; +pub(crate) mod rle; + +pub use bitmap_stream::*; +pub use rle::{RleDecodeError, RleEncodeError}; diff --git a/crates/ironrdp-graphics/src/rdp6/rle.rs b/crates/ironrdp-graphics/src/rdp6/rle.rs new file mode 100644 index 00000000..7e9a66fc --- /dev/null +++ b/crates/ironrdp-graphics/src/rdp6/rle.rs @@ -0,0 +1,720 @@ +use core::cmp; +use std::io::{Read as _, Write as _}; + +use byteorder::ReadBytesExt as _; +use ironrdp_core::WriteCursor; + +/// Maximum possible segment size is 47 (run_length = 2, raw_bytes_count = 15), which is treated as +/// special mode segment, which repeats last decoded byte in scanline 32 + raw_bytes_count times +const MAX_DECODED_SEGMENT_SIZE: usize = 47; + +#[derive(Debug)] +pub enum RleDecodeError { + ReadCompressedData(std::io::Error), + WriteDecompressedData(std::io::Error), + InvalidSegmentHeader, + SegmentDoNotFitScanline, +} + +impl core::fmt::Display for RleDecodeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RleDecodeError::ReadCompressedData(_error) => write!(f, "failed to read RLE-compressed data"), + RleDecodeError::WriteDecompressedData(_error) => write!(f, "failed to write decompressed data"), + RleDecodeError::InvalidSegmentHeader => write!(f, "invalid RLE segment header"), + RleDecodeError::SegmentDoNotFitScanline => { + write!(f, "decoded scanline segments length exceeds scanline length") + } + } + } +} + +impl core::error::Error for RleDecodeError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + RleDecodeError::ReadCompressedData(error) => Some(error), + RleDecodeError::WriteDecompressedData(error) => Some(error), + RleDecodeError::InvalidSegmentHeader => None, + RleDecodeError::SegmentDoNotFitScanline => None, + } + } +} + +#[derive(Debug)] +pub enum RleEncodeError { + NotEnoughBytes, + BufferTooSmall, +} + +impl core::fmt::Display for RleEncodeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RleEncodeError::NotEnoughBytes => write!(f, "not enough data to compress"), + RleEncodeError::BufferTooSmall => write!(f, "destination buffer is too small"), + } + } +} + +impl core::error::Error for RleEncodeError {} + +/// RLE-encoded color plane decoder implementation for RDP6 bitmap stream +#[derive(Debug)] +struct RlePlaneDecoder { + /// RDP6 performs per-scanline encoding, therefore segment decoder require state reset + /// for when each scanline is started (e.g. resetting last decoded byte value to 0) + last_decoded_byte: u8, + + width: usize, + height: usize, + + decoded_data: [u8; MAX_DECODED_SEGMENT_SIZE], + decoded_data_len: usize, +} + +impl RlePlaneDecoder { + fn new(width: usize, height: usize) -> Self { + Self { + last_decoded_byte: 0, + width, + height, + decoded_data: [0; MAX_DECODED_SEGMENT_SIZE], + decoded_data_len: 0, + } + } + + fn decompress_next_segment(&mut self, mut src: &[u8]) -> Result { + let control_byte = src.read_u8().map_err(RleDecodeError::ReadCompressedData)?; + + if control_byte == 0 { + return Err(RleDecodeError::InvalidSegmentHeader); + } + + let rle_bytes_field = control_byte & 0x0F; + let raw_bytes_field = (control_byte >> 4) & 0x0F; + + let (run_length, raw_bytes_count) = match rle_bytes_field { + 1 => (16 + usize::from(raw_bytes_field), 0), + 2 => (32 + usize::from(raw_bytes_field), 0), + rle_control => (usize::from(rle_control), usize::from(raw_bytes_field)), + }; + + self.decoded_data_len = raw_bytes_count + run_length; + + src.read_exact(&mut self.decoded_data[..raw_bytes_count]) + .map_err(RleDecodeError::ReadCompressedData)?; + + if raw_bytes_count > 0 { + // save last decoded byte for the next segments decoding + self.last_decoded_byte = self.decoded_data[raw_bytes_count - 1]; + } + + self.decoded_data[raw_bytes_count..self.decoded_data_len].fill(self.last_decoded_byte); + + Ok(raw_bytes_count + 1) + } + + /// Decodes single RLE-encoded scanline, without performing delta transformation + fn decode_scanline(&mut self, src: &[u8], mut dst: &mut [u8]) -> Result { + let mut decoded_columns = 0; + let mut read_bytes = 0; + + self.last_decoded_byte = 0; + + while decoded_columns < self.width { + read_bytes += self.decompress_next_segment(&src[read_bytes..])?; + + if decoded_columns + self.decoded_data_len > self.width { + return Err(RleDecodeError::SegmentDoNotFitScanline); + } + + dst.write_all(&self.decoded_data[..self.decoded_data_len]) + .map_err(RleDecodeError::WriteDecompressedData)?; + + decoded_columns += self.decoded_data_len; + } + + Ok(read_bytes) + } + + /// Performs delta transformation as described in 3.1.9.2.3 of [MS-RDPEGDI] + fn resolve_scanline_delta(prev_line: &[u8], current_scanline: &mut [u8]) { + assert!(prev_line.len() == current_scanline.len()); + + current_scanline + .iter_mut() + .zip(prev_line.iter()) + .for_each(|(dst, src)| { + let delta = *dst; + let value_above = *src; + + let transformed_delta = if delta % 2 == 1 { + 255u8.wrapping_sub((delta.wrapping_sub(1)) >> 1) + } else { + delta >> 1 + }; + + *dst = value_above.wrapping_add(transformed_delta); + }); + } + + fn decode(mut self, src: &[u8], dst: &mut [u8]) -> Result { + let mut read_bytes = 0; + + read_bytes += self.decode_scanline(src, dst)?; + + let (mut prev_scanline, mut dst) = dst.split_at_mut(self.width); + + for _ in 1..self.height { + let current_scanline = &mut dst[..self.width]; + + read_bytes += self.decode_scanline(&src[read_bytes..], current_scanline)?; + Self::resolve_scanline_delta(prev_scanline, current_scanline); + + (prev_scanline, dst) = dst.split_at_mut(self.width); + } + + Ok(read_bytes) + } +} + +/// Performs decompression of 8bpp color plane into slice. +/// Slice must have enough space for decompressed data. +/// Size of data written to dst buffer is exactly equal to `width * height`. +/// +/// Returns number of bytes consumed from src buffer. +pub(crate) fn decompress_8bpp_plane( + src: &[u8], + dst: &mut [u8], + width: usize, + height: usize, +) -> Result { + RlePlaneDecoder::new(width, height).decode(src, dst) +} + +struct RleEncoderScanlineIterator { + inner: core::iter::Enumerate, + width: usize, + prev_scanline: Vec, +} + +impl RleEncoderScanlineIterator { + fn new(width: usize, inner: I) -> Self { + Self { + width, + inner: inner.enumerate(), + prev_scanline: vec![0; width], + } + } + + fn delta_value(prev: u8, next: u8) -> u8 { + let mut result = u8::try_from((i16::from(next) - i16::from(prev)) & 0xFF) + .expect("masking with 0xFF ensures that the value fits into u8"); + + // bit magic from 3.1.9.2.1 of [MS-RDPEGDI]. + if result < 128 { + result <<= 1; + } else { + result = (255u8.wrapping_sub(result) << 1).wrapping_add(1); + } + + result + } +} + +impl> Iterator for RleEncoderScanlineIterator { + type Item = I::Item; + + fn next(&mut self) -> Option { + let (idx, mut next) = self.inner.next()?; + + let prev = core::mem::replace(&mut self.prev_scanline[idx % self.width], next); + if idx >= self.width { + next = Self::delta_value(prev, next); + } + + Some(next) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +#[derive(Debug)] +struct RlePlaneEncoder { + width: usize, + height: usize, +} + +macro_rules! ensure_size { + (dst: $buf:ident, size: $expected:expr) => {{ + let available = $buf.len(); + let needed = $expected; + if !(needed <= available) { + return Err(RleEncodeError::BufferTooSmall); + } + }}; +} + +impl RlePlaneEncoder { + fn new(width: usize, height: usize) -> Self { + Self { width, height } + } + + fn encode(&self, mut src: impl Iterator, dst: &mut WriteCursor<'_>) -> Result { + let mut written = 0; + + for _ in 0..self.height { + written += self.encode_scanline((&mut src).take(self.width), dst)?; + } + + Ok(written) + } + + fn encode_scanline( + &self, + mut src: impl Iterator, + dst: &mut WriteCursor<'_>, + ) -> Result { + let mut written = 0; + let first = src.next().ok_or(RleEncodeError::NotEnoughBytes)?; + + let mut raw = vec![first]; + let mut seq = (first, 0); + + for byte in src { + let (last, count) = seq; + + seq = if byte == last { + (byte, count + 1) + } else { + match count { + 3.. => { + written += self.encode_segment(&raw, count, dst)?; + raw.clear(); + } + 2 => raw.extend_from_slice(&[last, last]), + 1 => raw.push(last), + _ => {} + } + + raw.push(byte); + + (byte, 0) + } + } + + let (last, mut count) = seq; + if count < 3 { + raw.extend(vec![last; count]); + count = 0; + } + + written += self.encode_segment(&raw, count, dst)?; + + Ok(written) + } + + fn encode_segment(&self, mut raw: &[u8], run: usize, dst: &mut WriteCursor<'_>) -> Result { + if raw.is_empty() { + return Err(RleEncodeError::NotEnoughBytes); + } + + let mut extra_bytes = 0; + + while raw.len() > 15 { + extra_bytes += self.encode_segment(&raw[0..15], 0, dst)?; + raw = &raw[15..]; + } + + let raw_len = u8::try_from(raw.len()).expect("max value is guaranteed to be 15 due to the prior while loop"); + let run_capped = u8::try_from(cmp::min(run, 15)).expect("max value is guaranteed to be 15"); + + let control = (raw_len << 4) + run_capped; + + ensure_size!(dst: dst, size: raw.len() + 1); + + dst.write_u8(control); + dst.write_slice(raw); + + if run > 15 { + let last = raw.last().expect("buffer cannot be empty"); + extra_bytes += self.encode_long_sequence(run - 15, *last, dst)?; + } + + Ok(1 + raw.len() + extra_bytes) + } + + fn encode_long_sequence( + &self, + mut run: usize, + last: u8, + dst: &mut WriteCursor<'_>, + ) -> Result { + let mut written = 0; + + while run >= 16 { + ensure_size!(dst: dst, size: 1); + + let current = u8::try_from(cmp::min(run, MAX_DECODED_SEGMENT_SIZE)) + .expect("max value is guaranteed to be MAX_DECODED_SEGMENT_SIZE (47)"); + + let c_raw_bytes = cmp::min(current / 16, 2); + let n_run_length = current - c_raw_bytes * 16; + + let control = (n_run_length << 4) + c_raw_bytes; + dst.write_u8(control); + written += 1; + + run -= usize::from(current); + } + + if run > 0 { + match run { + short @ 1..=3 => { + written += self.encode_segment(&vec![last; short], 0, dst)?; + } + long => { + written += self.encode_segment(&[last], long - 1, dst)?; + } + } + } + + Ok(written) + } +} + +/// Performs compression of 8bpp color plane pixel stream into a buffer. +/// Pixel iterator must have at least width * height items. +/// Destination slice must have enough space for the compressed data. +/// +/// Returns number of bytes written to the dst buffer. +pub(crate) fn compress_8bpp_plane( + src: impl Iterator, + dst: &mut WriteCursor<'_>, + width: usize, + height: usize, +) -> Result { + let iter = RleEncoderScanlineIterator::new(width, src); + RlePlaneEncoder::new(width, height).encode(iter, dst) +} + +#[cfg(test)] +#[expect( + clippy::needless_raw_strings, + reason = "the lint is disable to not interfere with expect! macro" +)] +mod tests { + use expect_test::expect; + + use super::*; + + /// Performs decompression of 8bpp color plane into vector. Vector will be resized to fit decompressed data. + fn decompress(src: &[u8], dst: &mut Vec, width: usize, height: usize) -> Result { + // Ensure dest buffer have enough space for decompressed data + dst.resize(width * height, 0); + + decompress_8bpp_plane(src, dst.as_mut_slice(), width, height) + } + + fn compress(src: &[u8], dst: &mut [u8], width: usize, height: usize) -> Result { + compress_8bpp_plane(src.iter().copied(), &mut WriteCursor::new(dst), width, height) + } + + #[test] + fn simple_encode() { + // Example AAAABBCCCCCD from 3.1.9.2 of [MS-RDPEGDI]. + let src = [65, 65, 65, 65, 66, 66, 67, 67, 67, 67, 67, 68]; + + let width = src.len(); + let height = 1usize; + + let expected = &[0x13, 65, 0x34, 66, 66, 67, 0x10, 68]; + + let mut compressed = vec![0; 255]; + let len = compress(&src, &mut compressed, width, height).unwrap(); + + assert_eq!(&compressed[..len], expected); + } + + #[test] + fn long_sequence_encode() { + // Example from 3.1.9.2.2 of [MS-RDPEGDI]. + let src = [0x41u8; 100]; + + let width = 100usize; + let height = 1usize; + + let expected = &[0x1F, 0x41, 0xF2, 0x52]; + + let mut compressed = vec![0; 255]; + let len = compress(&src, &mut compressed, width, height).unwrap(); + + assert_eq!(&compressed[..len], expected); + } + + #[test] + fn multiline_encode() { + // Example from 3.1.9.2.1 of [MS-RDPEGDI]. + let src = [ + 255, 255, 255, 255, 254, 253, 254, 192, 132, 96, 75, 25, 253, 140, 62, 14, 135, 193, + ]; + + let width = 6usize; + let height = 3usize; + + let expected = &[ + 0x13, 0xFF, 0x20, 0xFE, 0xFD, 0x60, 0x01, 0x7D, 0xF5, 0xC2, 0x9A, 0x38, 0x60, 0x01, 0x67, 0x8B, 0xA3, 0x78, + 0xAF, + ]; + + let mut compressed = vec![0; 255]; + let len = compress(&src, &mut compressed, width, height).unwrap(); + + assert_eq!(&compressed[..len], expected); + } + + #[test] + fn long_sequence_decode() { + // Example from 3.1.9.2.2 of [MS-RDPEGDI]. + let src = [0x1F, 0x41, 0xF2, 0x52]; + + let width = 100usize; + let height = 1usize; + + let expected = &[0x41u8; 100]; + + let mut actual = Vec::new(); + decompress(&src, &mut actual, width, height).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn multiline_decode() { + // Example from 3.1.9.2.3 of [MS-RDPEGDI]. + let src = [ + 0x13, 0xFF, 0x20, 0xFE, 0xFD, 0x60, 0x01, 0x7D, 0xF5, 0xC2, 0x9A, 0x38, 0x60, 0x01, 0x67, 0x8B, 0xA3, 0x78, + 0xAF, + ]; + + let width = 6usize; + let height = 3usize; + + let expected = &[ + 255, 255, 255, 255, 254, 253, 254, 192, 132, 96, 75, 25, 253, 140, 62, 14, 135, 193, + ]; + + let mut actual = Vec::new(); + decompress(&src, &mut actual, width, height).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn long_sequence_encode_decode() { + // Example from 3.1.9.2.2 of [MS-RDPEGDI]. + let src = [0x41u8; 100]; + + let width = 100usize; + let height = 1usize; + + let mut compressed = vec![0; 255]; + let len = compress(&src, &mut compressed, width, height).unwrap(); + + let mut actual = Vec::new(); + decompress(&compressed[..len], &mut actual, width, height).unwrap(); + + assert_eq!(actual.as_slice(), src.as_slice()); + } + + #[test] + fn complex_encode_decode() { + let src = [ + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 18, 18, 18, 19, 19, 18, 18, 18, + 18, 18, 18, 18, 18, + ]; + + let width = src.len(); + let height = 1usize; + + let mut compressed = vec![0; 255]; + let len = compress(&src, &mut compressed, width, height).unwrap(); + + let mut actual = Vec::new(); + decompress(&compressed[..len], &mut actual, width, height).unwrap(); + + assert_eq!(actual.as_slice(), src.as_slice()); + } + + #[test] + fn multiline_encode_decode() { + // Example from 3.1.9.2.3 of [MS-RDPEGDI]. + let src = [ + 255, 255, 255, 255, 254, 253, 254, 192, 132, 96, 75, 25, 253, 140, 62, 14, 135, 193, + ]; + + let width = 6usize; + let height = 3usize; + + let mut compressed = vec![0; 255]; + let len = compress(&src, &mut compressed, width, height).unwrap(); + + let mut actual = Vec::new(); + decompress(&compressed[..len], &mut actual, width, height).unwrap(); + + assert_eq!(actual.as_slice(), src.as_slice()); + } + + #[test] + fn each_scanline_resets_last_decoded_byte() { + let src = [0x17, 0xFF, 0x04, 0x40, 0x01, 0x02, 0x03, 0x04]; + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + + let expected = &[ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 0, 253, 1, + ]; + + decompress(&src, &mut actual, width, height).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn segments_out_of_scanline_produce_error() { + let src = [ + 0x18, 0xFF, // Will produce 9 bytes which is out of bounds for 8x2 image + 0x04, 0x40, 0x01, 0x02, 0x03, 0x04, + ]; + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + expect![[r#" + Err( + SegmentDoNotFitScanline, + ) + "#]] + .assert_debug_eq(&decompress(&src, &mut actual, width, height)); + + // Same test, but fail on non-first line + let src = [ + 0x17, 0xFF, 0x18, 0xFF, // Will produce 9 bytes which is out of bounds for 8x2 image + ]; + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + expect![[r#" + Err( + SegmentDoNotFitScanline, + ) + "#]] + .assert_debug_eq(&decompress(&src, &mut actual, width, height)); + } + + #[test] + fn insufficient_raw_bytes_handled() { + let src = [0x18]; // Actually require 1 more byte + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + expect![[r#" + Err( + ReadCompressedData( + Error { + kind: UnexpectedEof, + message: "failed to fill whole buffer", + }, + ), + ) + "#]] + .assert_debug_eq(&decompress(&src, &mut actual, width, height)); + } + + #[test] + fn empty_buffer_handled() { + let src = []; + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + expect![[r#" + Err( + ReadCompressedData( + Error { + kind: UnexpectedEof, + message: "failed to fill whole buffer", + }, + ), + ) + "#]] + .assert_debug_eq(&decompress(&src, &mut actual, width, height)); + } + + #[test] + fn buffer_too_small_encode() { + let src = [ + 255, 255, 255, 255, 254, 253, 254, 192, 132, 96, 75, 25, 253, 140, 62, 14, 135, 193, + ]; + + let width = 6usize; + let height = 3usize; + + let mut compressed = vec![0; 4]; + + expect![[r#" + Err( + BufferTooSmall, + ) + "#]] + .assert_debug_eq(&compress(&src, &mut compressed, width, height)); + } + + #[test] + fn not_enough_bytes_to_encode() { + let src = [255, 255, 255, 255, 254, 253, 254, 192, 132, 96, 75, 25, 253]; + + let width = 8usize; + let height = 3usize; + + let mut compressed = vec![0; 255]; + + expect![[r#" + Err( + NotEnoughBytes, + ) + "#]] + .assert_debug_eq(&compress(&src, &mut compressed, width, height)); + } + + #[test] + fn too_small_dest_buffer_handled() { + let src = [0x17, 0xFF, 0x04, 0x40, 0x01, 0x02, 0x03, 0x04]; + + let width = 8usize; + let height = 2usize; + + let mut actual = vec![0u8; 7]; + + expect![[r#" + Err( + WriteDecompressedData( + Error { + kind: WriteZero, + message: "failed to write whole buffer", + }, + ), + ) + "#]] + .assert_debug_eq(&decompress_8bpp_plane(&src, &mut actual, width, height)); + + // Check same failure mode, but on non-first line + } +} diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/32x64_rgb_raw.bin b/crates/ironrdp-graphics/src/rdp6/test_assets/32x64_rgb_raw.bin new file mode 100644 index 00000000..ae0608bc Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/32x64_rgb_raw.bin differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/32x64_rgb_raw.bmp b/crates/ironrdp-graphics/src/rdp6/test_assets/32x64_rgb_raw.bmp new file mode 100644 index 00000000..77a5f94d Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/32x64_rgb_raw.bmp differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x24_argb_rle.bin b/crates/ironrdp-graphics/src/rdp6/test_assets/64x24_argb_rle.bin new file mode 100644 index 00000000..2762c667 Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x24_argb_rle.bin differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x24_argb_rle.bmp b/crates/ironrdp-graphics/src/rdp6/test_assets/64x24_argb_rle.bmp new file mode 100644 index 00000000..e72fdf65 Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x24_argb_rle.bmp differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x35_ycocg_rle_ss.bin b/crates/ironrdp-graphics/src/rdp6/test_assets/64x35_ycocg_rle_ss.bin new file mode 100644 index 00000000..7f0d64fc Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x35_ycocg_rle_ss.bin differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x35_ycocg_rle_ss.bmp b/crates/ironrdp-graphics/src/rdp6/test_assets/64x35_ycocg_rle_ss.bmp new file mode 100644 index 00000000..b87630b5 Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x35_ycocg_rle_ss.bmp differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_aycocg_rle.bin b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_aycocg_rle.bin new file mode 100644 index 00000000..46560493 Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_aycocg_rle.bin differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_aycocg_rle.bmp b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_aycocg_rle.bmp new file mode 100644 index 00000000..0c53eea2 Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_aycocg_rle.bmp differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bin b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bin new file mode 100644 index 00000000..2ad571ac Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bin differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bmp b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bmp new file mode 100644 index 00000000..4a08aa52 Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bmp differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_rle_ss.bin b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_rle_ss.bin new file mode 100644 index 00000000..a7482b77 Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_rle_ss.bin differ diff --git a/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_rle_ss.bmp b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_rle_ss.bmp new file mode 100644 index 00000000..64379db7 Binary files /dev/null and b/crates/ironrdp-graphics/src/rdp6/test_assets/64x64_ycocg_rle_ss.bmp differ diff --git a/crates/ironrdp-graphics/src/rectangle_processing.rs b/crates/ironrdp-graphics/src/rectangle_processing.rs new file mode 100644 index 00000000..eaffb39e --- /dev/null +++ b/crates/ironrdp-graphics/src/rectangle_processing.rs @@ -0,0 +1,1636 @@ +use core::cmp::{max, min}; + +use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; + +// TODO(@pacmancoder): This code currently works only on `InclusiveRectangle`, but it should be +// made generic over `Rectangle` trait + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Region { + pub extents: InclusiveRectangle, + pub rectangles: Vec, +} + +impl Region { + pub fn new() -> Self { + Self { + extents: InclusiveRectangle::empty(), + rectangles: Vec::new(), + } + } + + pub fn union_rectangle(&mut self, rectangle: InclusiveRectangle) { + if self.rectangles.is_empty() { + *self = Self::from(rectangle); + } else { + let mut dst = Vec::with_capacity(self.rectangles.len() + 1); + + handle_rectangle_higher_relative_to_extents(&rectangle, &self.extents, &mut dst); + + // treat possibly overlapping region + let bands = split_bands(self.rectangles.as_slice()); + let mut bands = bands.as_slice(); + while let Some((band, bands_new)) = bands.split_first() { + bands = bands_new; + + let top_inter_band = if band[0].bottom <= rectangle.top + || rectangle.bottom <= band[0].top + || rectangle_in_band(band, &rectangle) + { + // `rectangle` is lower, higher, or in the current band + dst.extend_from_slice(band); + + rectangle.top + } else { + handle_rectangle_that_overlaps_band(&rectangle, band, &mut dst); + + band[0].bottom + }; + + if !bands.is_empty() { + let next_band = bands[0]; + handle_rectangle_between_bands(&rectangle, band, next_band, &mut dst, top_inter_band); + } + } + + handle_rectangle_lower_relative_to_extents(&rectangle, &self.extents, &mut dst); + + self.rectangles = dst; + self.extents = self.extents.union(&rectangle); + + self.simplify(); + } + } + + #[must_use] + pub fn intersect_rectangle(&self, rectangle: &InclusiveRectangle) -> Self { + match self.rectangles.len() { + 0 => Self::new(), + 1 => self.extents.intersect(rectangle).map(Self::from).unwrap_or_default(), + _ => { + let rectangles = self + .rectangles + .iter() + .take_while(|r| r.top <= rectangle.bottom) + .filter_map(|r| r.intersect(rectangle)) + .collect::>(); + let extents = InclusiveRectangle::union_all(rectangles.as_slice()); + + let mut region = Self { rectangles, extents }; + region.simplify(); + + region + } + } + } + + fn simplify(&mut self) { + /* Simplify consecutive bands that touch and have the same items + * + * ==================== ==================== + * | 1 | | 2 | | | | | + * ==================== | | | | + * | 1 | | 2 | ====> | 1 | | 2 | + * ==================== | | | | + * | 1 | | 2 | | | | | + * ==================== ==================== + * + */ + + if self.rectangles.len() < 2 { + return; + } + + let mut current_band_start = 0; + while current_band_start < self.rectangles.len() + && current_band_start + get_current_band(&self.rectangles[current_band_start..]).len() + < self.rectangles.len() + { + let current_band = get_current_band(&self.rectangles[current_band_start..]); + let next_band = get_current_band(&self.rectangles[current_band_start + current_band.len()..]); + + if current_band[0].bottom == next_band[0].top && bands_internals_equal(current_band, next_band) { + let first_band_len = current_band.len(); + let second_band_len = next_band.len(); + let second_band_bottom = next_band[0].bottom; + self.rectangles + .drain(current_band_start + first_band_len..current_band_start + first_band_len + second_band_len); + self.rectangles + .iter_mut() + .skip(current_band_start) + .take(first_band_len) + .for_each(|r| r.bottom = second_band_bottom); + } else { + current_band_start += current_band.len(); + } + } + } +} + +impl Default for Region { + fn default() -> Self { + Self::new() + } +} + +impl From for Region { + fn from(r: InclusiveRectangle) -> Self { + Self { + extents: r.clone(), + rectangles: vec![r], + } + } +} + +fn handle_rectangle_higher_relative_to_extents( + rectangle: &InclusiveRectangle, + extents: &InclusiveRectangle, + dst: &mut Vec, +) { + if rectangle.top < extents.top { + dst.push(InclusiveRectangle { + top: rectangle.top, + bottom: min(extents.top, rectangle.bottom), + left: rectangle.left, + right: rectangle.right, + }); + } +} + +fn handle_rectangle_lower_relative_to_extents( + rectangle: &InclusiveRectangle, + extents: &InclusiveRectangle, + dst: &mut Vec, +) { + if extents.bottom < rectangle.bottom { + dst.push(InclusiveRectangle { + top: max(extents.bottom, rectangle.top), + bottom: rectangle.bottom, + left: rectangle.left, + right: rectangle.right, + }); + } +} + +fn handle_rectangle_that_overlaps_band( + rectangle: &InclusiveRectangle, + band: &[InclusiveRectangle], + dst: &mut Vec, +) { + /* rect overlaps the band: + | | | | + ====^=================| |==| |=========================== band + | top split | | | | + v | 1 | | 2 | + ^ | | | | +----+ +----+ + | merge zone | | | | | | | 4 | + v +----+ | | | | +----+ + ^ | | | 3 | + | bottom split | | | | + ====v=========================| |==| |=================== + | | | | + + possible cases: + 1) no top split, merge zone then a bottom split. The band will be split + in two + 2) not band split, only the merge zone, band merged with rect but not split + 3) a top split, the merge zone and no bottom split. The band will be split + in two + 4) a top split, the merge zone and also a bottom split. The band will be + split in 3, but the coalesce algorithm may merge the created bands + */ + + let band_top = band[0].top; + let band_bottom = band[0].bottom; + + if band_top < rectangle.top { + // split current band by the current band top and `rectangle.top` (case 3, 4) + copy_band(band, dst, band_top, rectangle.top); + } + + // split the merge zone (all cases) + copy_band_with_union( + band, + dst, + max(rectangle.top, band_top), + min(rectangle.bottom, band_bottom), + rectangle, + ); + + // split current band by the `rectangle.bottom` and the current band bottom (case 1, 4) + if rectangle.bottom < band_bottom { + copy_band(band, dst, rectangle.bottom, band_bottom); + } +} + +fn handle_rectangle_between_bands( + rectangle: &InclusiveRectangle, + band: &[InclusiveRectangle], + next_band: &[InclusiveRectangle], + dst: &mut Vec, + top_inter_band: u16, +) { + /* test if a piece of rect should be inserted as a new band between + * the current band and the next one. band n and n+1 shouldn't touch. + * + * ============================================================== + * band n + * +------+ +------+ + * ===========| rect |====================| |=============== + * | | +------+ | | + * +------+ | rect | | rect | + * +------+ | | + * =======================================| |================ + * +------+ band n+1 + * =============================================================== + * + */ + + let band_bottom = band[0].bottom; + + let next_band_top = next_band[0].top; + if next_band_top != band_bottom && band_bottom < rectangle.bottom && rectangle.top < next_band_top { + dst.push(InclusiveRectangle { + top: top_inter_band, + bottom: min(next_band_top, rectangle.bottom), + left: rectangle.left, + right: rectangle.right, + }); + } +} + +fn rectangle_in_band(band: &[InclusiveRectangle], rectangle: &InclusiveRectangle) -> bool { + // part of `rectangle` is higher or lower + if rectangle.top < band[0].top || band[0].bottom < rectangle.bottom { + return false; + } + + for source_rectangle in band { + if source_rectangle.left <= rectangle.left { + if rectangle.right <= source_rectangle.right { + return true; + } + } else { + // as the band is sorted from left to right, + // once we've seen an item that is after `rectangle.left` + // we are sure that the result is false + return false; + } + } + + false +} + +fn copy_band_with_union( + mut band: &[InclusiveRectangle], + dst: &mut Vec, + band_top: u16, + band_bottom: u16, + union_rectangle: &InclusiveRectangle, +) { + /* merges a band with the given rect + * Input: + * unionRect + * | | + * | | + * ==============+===============+================================ + * |Item1| |Item2| |Item3| |Item4| |Item5| Band + * ==============+===============+================================ + * before | overlap | after + * + * Resulting band: + * +-----+ +----------------------+ +-----+ + * |Item1| | Item2 | |Item3| + * +-----+ +----------------------+ +-----+ + * + * We first copy as-is items that are before Item2, the first overlapping + * item. + * Then we find the last one that overlap unionRect to aggregate Item2, Item3 + * and Item4 to create Item2. + * Finally Item5 is copied as Item3. + * + * When no unionRect is provided, we skip the two first steps to just copy items + */ + + let items_before_union_rectangle = band + .iter() + .map(|r| InclusiveRectangle { + top: band_top, + bottom: band_bottom, + left: r.left, + right: r.right, + }) + .take_while(|r| r.right < union_rectangle.left); + let items_before_union_rectangle_len = items_before_union_rectangle.clone().map(|_| 1).sum::(); + dst.extend(items_before_union_rectangle); + band = &band[items_before_union_rectangle_len..]; + + // treat items overlapping with `union_rectangle` + let left = min( + band.first().map(|r| r.left).unwrap_or(union_rectangle.left), + union_rectangle.left, + ); + let mut right = union_rectangle.right; + while !band.is_empty() { + if band[0].right >= union_rectangle.right { + if band[0].left < union_rectangle.right { + right = band[0].right; + band = &band[1..]; + } + break; + } + band = &band[1..]; + } + dst.push(InclusiveRectangle { + top: band_top, + bottom: band_bottom, + left, + right, + }); + + // treat remaining items on the same band + copy_band(band, dst, band_top, band_bottom); +} + +fn copy_band(band: &[InclusiveRectangle], dst: &mut Vec, band_top: u16, band_bottom: u16) { + dst.extend(band.iter().map(|r| InclusiveRectangle { + top: band_top, + bottom: band_bottom, + left: r.left, + right: r.right, + })); +} + +fn split_bands(mut rectangles: &[InclusiveRectangle]) -> Vec<&[InclusiveRectangle]> { + let mut bands = Vec::new(); + while !rectangles.is_empty() { + let band = get_current_band(rectangles); + rectangles = &rectangles[band.len()..]; + bands.push(band); + } + + bands +} + +fn get_current_band(rectangles: &[InclusiveRectangle]) -> &[InclusiveRectangle] { + let band_top = rectangles[0].top; + + for i in 1..rectangles.len() { + if rectangles[i].top != band_top { + return &rectangles[..i]; + } + } + + rectangles +} + +fn bands_internals_equal(first_band: &[InclusiveRectangle], second_band: &[InclusiveRectangle]) -> bool { + if first_band.len() != second_band.len() { + return false; + } + + for (first_band_rect, second_band_rect) in first_band.iter().zip(second_band.iter()) { + if first_band_rect.left != second_band_rect.left || first_band_rect.right != second_band_rect.right { + return false; + } + } + + true +} + +#[cfg(test)] +mod tests { + use std::sync::LazyLock; + + use super::*; + + static REGION_FOR_RECTANGLES_INTERSECTION: LazyLock = LazyLock::new(|| Region { + extents: InclusiveRectangle { + left: 1, + top: 1, + right: 11, + bottom: 9, + }, + rectangles: vec![ + InclusiveRectangle { + left: 1, + top: 1, + right: 5, + bottom: 3, + }, + InclusiveRectangle { + left: 7, + top: 1, + right: 8, + bottom: 3, + }, + InclusiveRectangle { + left: 9, + top: 1, + right: 11, + bottom: 3, + }, + InclusiveRectangle { + left: 7, + top: 3, + right: 11, + bottom: 4, + }, + InclusiveRectangle { + left: 3, + top: 4, + right: 6, + bottom: 6, + }, + InclusiveRectangle { + left: 7, + top: 4, + right: 11, + bottom: 6, + }, + InclusiveRectangle { + left: 1, + top: 6, + right: 3, + bottom: 8, + }, + InclusiveRectangle { + left: 4, + top: 6, + right: 5, + bottom: 8, + }, + InclusiveRectangle { + left: 6, + top: 6, + right: 10, + bottom: 8, + }, + InclusiveRectangle { + left: 4, + top: 8, + right: 5, + bottom: 9, + }, + InclusiveRectangle { + left: 6, + top: 8, + right: 10, + bottom: 9, + }, + ], + }); + + #[test] + fn union_rectangle_sets_extents_and_single_rectangle_for_empty_region() { + let mut region = Region::new(); + + let input_rectangle = InclusiveRectangle { + left: 5, + top: 1, + right: 9, + bottom: 2, + }; + + let expected_region = Region { + extents: input_rectangle.clone(), + rectangles: vec![input_rectangle.clone()], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_places_new_rectangle_higher_relative_to_band() { + let existing_band_rectangle = InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }; + let mut region = Region { + extents: existing_band_rectangle.clone(), + rectangles: vec![existing_band_rectangle.clone()], + }; + + let input_rectangle = InclusiveRectangle { + left: 5, + top: 1, + right: 9, + bottom: 2, + }; + + let expected_region = Region { + extents: InclusiveRectangle { + left: 2, + top: 1, + right: 9, + bottom: 7, + }, + rectangles: vec![input_rectangle.clone(), existing_band_rectangle], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_places_new_rectangle_lower_relative_to_band() { + let existing_band_rectangle = InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }; + let mut region = Region { + extents: existing_band_rectangle.clone(), + rectangles: vec![existing_band_rectangle.clone()], + }; + + let input_rectangle = InclusiveRectangle { + left: 1, + top: 8, + right: 4, + bottom: 10, + }; + + let expected_region = Region { + extents: InclusiveRectangle { + left: 1, + top: 3, + right: 7, + bottom: 10, + }, + rectangles: vec![existing_band_rectangle, input_rectangle.clone()], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_does_not_add_new_rectangle_which_is_inside_a_band() { + let existing_band_rectangle = InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }; + let mut region = Region { + extents: existing_band_rectangle.clone(), + rectangles: vec![existing_band_rectangle.clone()], + }; + + let input_rectangle = InclusiveRectangle { + left: 5, + top: 4, + right: 6, + bottom: 5, + }; + + let expected_region = Region { + extents: existing_band_rectangle.clone(), + rectangles: vec![existing_band_rectangle], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_cuts_new_rectangle_top_part_which_crosses_band_on_top() { + let existing_band_rectangle = InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }; + let mut region = Region { + extents: existing_band_rectangle.clone(), + rectangles: vec![existing_band_rectangle], + }; + + let input_rectangle = InclusiveRectangle { + left: 1, + top: 2, + right: 4, + bottom: 4, + }; + + let expected_region = Region { + extents: InclusiveRectangle { + left: 1, + top: 2, + right: 7, + bottom: 7, + }, + rectangles: vec![ + InclusiveRectangle { + left: 1, + top: 2, + right: 4, + bottom: 3, + }, + InclusiveRectangle { + left: 1, + top: 3, + right: 7, + bottom: 4, + }, + InclusiveRectangle { + left: 2, + top: 4, + right: 7, + bottom: 7, + }, + ], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_cuts_new_rectangle_lower_part_which_crosses_band_on_bottom() { + let existing_band_rectangle = InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }; + let mut region = Region { + extents: existing_band_rectangle.clone(), + rectangles: vec![existing_band_rectangle], + }; + + let input_rectangle = InclusiveRectangle { + left: 5, + top: 6, + right: 9, + bottom: 8, + }; + + let expected_region = Region { + extents: InclusiveRectangle { + left: 2, + top: 3, + right: 9, + bottom: 8, + }, + rectangles: vec![ + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 6, + }, + InclusiveRectangle { + left: 2, + top: 6, + right: 9, + bottom: 7, + }, + InclusiveRectangle { + left: 5, + top: 7, + right: 9, + bottom: 8, + }, + ], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_cuts_new_rectangle_higher_and_lower_part_which_crosses_band_on_top_and_bottom() { + let existing_band_rectangle = InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }; + let mut region = Region { + extents: existing_band_rectangle.clone(), + rectangles: vec![existing_band_rectangle], + }; + + let input_rectangle = InclusiveRectangle { + left: 3, + top: 1, + right: 5, + bottom: 11, + }; + + let expected_region = Region { + extents: InclusiveRectangle { + left: 2, + top: 1, + right: 7, + bottom: 11, + }, + rectangles: vec![ + InclusiveRectangle { + left: 3, + top: 1, + right: 5, + bottom: 3, + }, + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 3, + top: 7, + right: 5, + bottom: 11, + }, + ], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_inserts_new_rectangle_in_band_of_3_rectangles_without_merging_with_rectangles() { + let mut region = Region { + extents: InclusiveRectangle { + left: 2, + top: 3, + right: 15, + bottom: 7, + }, + rectangles: vec![ + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 8, + top: 3, + right: 9, + bottom: 7, + }, + InclusiveRectangle { + left: 12, + top: 3, + right: 15, + bottom: 7, + }, + ], + }; + + let input_rectangle = InclusiveRectangle { + left: 10, + top: 3, + right: 11, + bottom: 7, + }; + let expected_region = Region { + extents: InclusiveRectangle { + left: 2, + top: 3, + right: 15, + bottom: 7, + }, + rectangles: vec![ + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 8, + top: 3, + right: 9, + bottom: 7, + }, + InclusiveRectangle { + left: 10, + top: 3, + right: 11, + bottom: 7, + }, + InclusiveRectangle { + left: 12, + top: 3, + right: 15, + bottom: 7, + }, + ], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_inserts_new_rectangle_in_band_of_3_rectangles_with_merging_with_side_rectangles() { + let mut region = Region { + extents: InclusiveRectangle { + left: 2, + top: 3, + right: 15, + bottom: 7, + }, + rectangles: vec![ + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 8, + top: 3, + right: 10, + bottom: 7, + }, + InclusiveRectangle { + left: 13, + top: 3, + right: 15, + bottom: 7, + }, + ], + }; + + let input_rectangle = InclusiveRectangle { + left: 9, + top: 3, + right: 14, + bottom: 7, + }; + let expected_region = Region { + extents: InclusiveRectangle { + left: 2, + top: 3, + right: 15, + bottom: 7, + }, + rectangles: vec![ + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 8, + top: 3, + right: 15, + bottom: 7, + }, + ], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_inserts_new_rectangle_in_band_of_3_rectangles_with_merging_with_side_rectangles_on_board() { + let mut region = Region { + extents: InclusiveRectangle { + left: 2, + top: 3, + right: 15, + bottom: 7, + }, + rectangles: vec![ + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 8, + top: 3, + right: 10, + bottom: 7, + }, + InclusiveRectangle { + left: 13, + top: 3, + right: 15, + bottom: 7, + }, + ], + }; + + let input_rectangle = InclusiveRectangle { + left: 10, + top: 3, + right: 13, + bottom: 7, + }; + let expected_region = Region { + extents: InclusiveRectangle { + left: 2, + top: 3, + right: 15, + bottom: 7, + }, + rectangles: vec![ + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 8, + top: 3, + right: 13, + bottom: 7, + }, + InclusiveRectangle { + left: 13, + top: 3, + right: 15, + bottom: 7, + }, + ], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn union_rectangle_inserts_new_rectangle_between_two_bands() { + let mut region = Region { + extents: InclusiveRectangle { + left: 1, + top: 3, + right: 7, + bottom: 10, + }, + rectangles: vec![ + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 1, + top: 8, + right: 4, + bottom: 10, + }, + ], + }; + + let input_rectangle = InclusiveRectangle { + left: 3, + top: 4, + right: 4, + bottom: 9, + }; + let expected_region = Region { + extents: InclusiveRectangle { + left: 1, + top: 3, + right: 7, + bottom: 10, + }, + rectangles: vec![ + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 3, + top: 7, + right: 4, + bottom: 8, + }, + InclusiveRectangle { + left: 1, + top: 8, + right: 4, + bottom: 10, + }, + ], + }; + + region.union_rectangle(input_rectangle); + assert_eq!(expected_region, region); + } + + #[test] + fn simplify_does_not_change_two_different_bands_with_multiple_rectangles() { + let mut region = Region { + extents: InclusiveRectangle { + left: 1, + top: 1, + right: 7, + bottom: 3, + }, + rectangles: vec![ + InclusiveRectangle { + left: 1, + top: 1, + right: 2, + bottom: 2, + }, + InclusiveRectangle { + left: 3, + top: 1, + right: 4, + bottom: 2, + }, + InclusiveRectangle { + left: 5, + top: 1, + right: 6, + bottom: 2, + }, + InclusiveRectangle { + left: 1, + top: 2, + right: 2, + bottom: 3, + }, + InclusiveRectangle { + left: 3, + top: 2, + right: 4, + bottom: 3, + }, + InclusiveRectangle { + left: 5, + top: 2, + right: 7, + bottom: 3, + }, + ], + }; + let expected_region = region.clone(); + + region.simplify(); + assert_eq!(expected_region, region); + } + + #[test] + fn simplify_does_not_change_two_different_bands_with_one_rectangle() { + let mut region = Region { + extents: InclusiveRectangle { + left: 2, + top: 1, + right: 7, + bottom: 11, + }, + rectangles: vec![ + InclusiveRectangle { + left: 3, + top: 1, + right: 5, + bottom: 3, + }, + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + ], + }; + let expected_region = region.clone(); + + region.simplify(); + assert_eq!(expected_region, region); + } + + #[test] + fn simplify_does_not_change_three_different_bands_with_one_rectangle() { + let mut region = Region { + extents: InclusiveRectangle { + left: 2, + top: 1, + right: 7, + bottom: 11, + }, + rectangles: vec![ + InclusiveRectangle { + left: 3, + top: 1, + right: 5, + bottom: 3, + }, + InclusiveRectangle { + left: 2, + top: 3, + right: 7, + bottom: 7, + }, + InclusiveRectangle { + left: 3, + top: 7, + right: 5, + bottom: 11, + }, + ], + }; + let expected_region = region.clone(); + + region.simplify(); + assert_eq!(expected_region, region); + } + + #[test] + fn simplify_merges_bands_with_identical_internal_rectangles() { + let mut region = Region { + extents: InclusiveRectangle { + left: 1, + top: 1, + right: 7, + bottom: 3, + }, + rectangles: vec![ + InclusiveRectangle { + left: 1, + top: 1, + right: 2, + bottom: 2, + }, + InclusiveRectangle { + left: 3, + top: 1, + right: 4, + bottom: 2, + }, + InclusiveRectangle { + left: 5, + top: 1, + right: 6, + bottom: 2, + }, + InclusiveRectangle { + left: 1, + top: 2, + right: 2, + bottom: 3, + }, + InclusiveRectangle { + left: 3, + top: 2, + right: 4, + bottom: 3, + }, + InclusiveRectangle { + left: 5, + top: 2, + right: 6, + bottom: 3, + }, + ], + }; + let expected_region = Region { + extents: InclusiveRectangle { + left: 1, + top: 1, + right: 7, + bottom: 3, + }, + rectangles: vec![ + InclusiveRectangle { + left: 1, + top: 1, + right: 2, + bottom: 3, + }, + InclusiveRectangle { + left: 3, + top: 1, + right: 4, + bottom: 3, + }, + InclusiveRectangle { + left: 5, + top: 1, + right: 6, + bottom: 3, + }, + ], + }; + + region.simplify(); + assert_eq!(expected_region, region); + } + + #[test] + fn simplify_merges_three_bands_with_identical_internal_rectangles() { + let mut region = Region { + extents: InclusiveRectangle { + left: 1, + top: 1, + right: 7, + bottom: 3, + }, + rectangles: vec![ + InclusiveRectangle { + left: 1, + top: 1, + right: 2, + bottom: 2, + }, + InclusiveRectangle { + left: 3, + top: 1, + right: 4, + bottom: 2, + }, + InclusiveRectangle { + left: 5, + top: 1, + right: 6, + bottom: 2, + }, + InclusiveRectangle { + left: 1, + top: 2, + right: 2, + bottom: 3, + }, + InclusiveRectangle { + left: 3, + top: 2, + right: 4, + bottom: 3, + }, + InclusiveRectangle { + left: 5, + top: 2, + right: 6, + bottom: 3, + }, + InclusiveRectangle { + left: 1, + top: 3, + right: 2, + bottom: 4, + }, + InclusiveRectangle { + left: 3, + top: 3, + right: 4, + bottom: 4, + }, + InclusiveRectangle { + left: 5, + top: 3, + right: 6, + bottom: 4, + }, + ], + }; + let expected_region = Region { + extents: InclusiveRectangle { + left: 1, + top: 1, + right: 7, + bottom: 3, + }, + rectangles: vec![ + InclusiveRectangle { + left: 1, + top: 1, + right: 2, + bottom: 4, + }, + InclusiveRectangle { + left: 3, + top: 1, + right: 4, + bottom: 4, + }, + InclusiveRectangle { + left: 5, + top: 1, + right: 6, + bottom: 4, + }, + ], + }; + + region.simplify(); + assert_eq!(expected_region, region); + } + + #[test] + fn simplify_merges_two_pairs_of_bands_with_identical_internal_rectangles() { + let mut region = Region { + extents: InclusiveRectangle { + left: 1, + top: 1, + right: 7, + bottom: 5, + }, + rectangles: vec![ + InclusiveRectangle { + left: 1, + top: 1, + right: 2, + bottom: 2, + }, + InclusiveRectangle { + left: 3, + top: 1, + right: 4, + bottom: 2, + }, + InclusiveRectangle { + left: 5, + top: 1, + right: 6, + bottom: 2, + }, + InclusiveRectangle { + left: 1, + top: 2, + right: 2, + bottom: 3, + }, + InclusiveRectangle { + left: 3, + top: 2, + right: 4, + bottom: 3, + }, + InclusiveRectangle { + left: 5, + top: 2, + right: 6, + bottom: 3, + }, + InclusiveRectangle { + left: 2, + top: 3, + right: 3, + bottom: 4, + }, + InclusiveRectangle { + left: 4, + top: 3, + right: 5, + bottom: 4, + }, + InclusiveRectangle { + left: 6, + top: 3, + right: 7, + bottom: 4, + }, + InclusiveRectangle { + left: 2, + top: 4, + right: 3, + bottom: 5, + }, + InclusiveRectangle { + left: 4, + top: 4, + right: 5, + bottom: 5, + }, + InclusiveRectangle { + left: 6, + top: 4, + right: 7, + bottom: 5, + }, + ], + }; + let expected_region = Region { + extents: InclusiveRectangle { + left: 1, + top: 1, + right: 7, + bottom: 5, + }, + rectangles: vec![ + InclusiveRectangle { + left: 1, + top: 1, + right: 2, + bottom: 3, + }, + InclusiveRectangle { + left: 3, + top: 1, + right: 4, + bottom: 3, + }, + InclusiveRectangle { + left: 5, + top: 1, + right: 6, + bottom: 3, + }, + InclusiveRectangle { + left: 2, + top: 3, + right: 3, + bottom: 5, + }, + InclusiveRectangle { + left: 4, + top: 3, + right: 5, + bottom: 5, + }, + InclusiveRectangle { + left: 6, + top: 3, + right: 7, + bottom: 5, + }, + ], + }; + + region.simplify(); + assert_eq!(expected_region, region); + } + + #[test] + fn intersect_rectangle_returns_empty_region_for_not_intersecting_rectangle() { + let region = &*REGION_FOR_RECTANGLES_INTERSECTION; + let expected_region = Region { + extents: InclusiveRectangle { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + rectangles: Vec::new(), + }; + let input_rectangle = InclusiveRectangle { + left: 1, + top: 4, + right: 2, + bottom: 5, + }; + + let actual_region = region.intersect_rectangle(&input_rectangle); + assert_eq!(expected_region, actual_region); + } + + #[test] + fn intersect_rectangle_returns_empty_region_for_empty_intersection_region() { + let expected_region: Region = Region { + extents: InclusiveRectangle { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + rectangles: Vec::new(), + }; + let input_rectangle = InclusiveRectangle { + left: 5, + top: 2, + right: 6, + bottom: 3, + }; + + let actual_region = expected_region.intersect_rectangle(&input_rectangle); + assert_eq!(expected_region, actual_region); + } + + #[test] + fn intersect_rectangle_returns_part_of_rectangle_that_overlaps_for_region_with_one_rectangle() { + let region = Region { + extents: InclusiveRectangle { + left: 1, + top: 1, + right: 5, + bottom: 3, + }, + rectangles: vec![InclusiveRectangle { + left: 1, + top: 1, + right: 5, + bottom: 3, + }], + }; + let expected_region = Region { + extents: InclusiveRectangle { + left: 2, + top: 2, + right: 3, + bottom: 3, + }, + rectangles: vec![InclusiveRectangle { + left: 2, + top: 2, + right: 3, + bottom: 3, + }], + }; + let input_rectangle = InclusiveRectangle { + left: 2, + top: 2, + right: 3, + bottom: 3, + }; + + let actual_region = region.intersect_rectangle(&input_rectangle); + assert_eq!(expected_region, actual_region); + } + + #[test] + fn intersect_rectangle_returns_region_with_parts_of_rectangles_that_intersect_input_rectangle() { + let region = &*REGION_FOR_RECTANGLES_INTERSECTION; + let expected_region = Region { + extents: InclusiveRectangle { + left: 3, + top: 2, + right: 8, + bottom: 5, + }, + rectangles: vec![ + InclusiveRectangle { + left: 3, + top: 2, + right: 5, + bottom: 3, + }, + InclusiveRectangle { + left: 7, + top: 2, + right: 8, + bottom: 3, + }, + InclusiveRectangle { + left: 7, + top: 3, + right: 8, + bottom: 4, + }, + InclusiveRectangle { + left: 3, + top: 4, + right: 6, + bottom: 5, + }, + InclusiveRectangle { + left: 7, + top: 4, + right: 8, + bottom: 5, + }, + ], + }; + let input_rectangle = InclusiveRectangle { + left: 3, + top: 2, + right: 8, + bottom: 5, + }; + + let actual_region = region.intersect_rectangle(&input_rectangle); + assert_eq!(expected_region, actual_region); + } + + #[test] + fn intersect_rectangle_returns_region_with_exact_sizes_of_rectangle_that_overlaps_it() { + let region = &*REGION_FOR_RECTANGLES_INTERSECTION; + let expected_region = Region { + extents: InclusiveRectangle { + left: 2, + top: 2, + right: 4, + bottom: 3, + }, + rectangles: vec![InclusiveRectangle { + left: 2, + top: 2, + right: 4, + bottom: 3, + }], + }; + let input_rectangle: InclusiveRectangle = InclusiveRectangle { + left: 2, + top: 2, + right: 4, + bottom: 3, + }; + + let actual_region = region.intersect_rectangle(&input_rectangle); + assert_eq!(expected_region, actual_region); + } +} diff --git a/crates/ironrdp-graphics/src/rle.rs b/crates/ironrdp-graphics/src/rle.rs new file mode 100644 index 00000000..bea7c5c1 --- /dev/null +++ b/crates/ironrdp-graphics/src/rle.rs @@ -0,0 +1,863 @@ +//! Interleaved Run-Length Encoding (RLE) Bitmap Codec +//! +//! # References +//! +//! - Microsoft Learn: +//! - [RLE_BITMAP_STREAM](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/b3b60873-16a8-4cbc-8aaa-5f0a93083280) +//! - [Pseudo-code](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/b6a3f5c2-0804-4c10-9d25-a321720fd23e) +//! +//! - FreeRDP: +//! - [interleaved.c](https://github.com/FreeRDP/FreeRDP/blob/db98f16e5bce003c898e8c85eb7af964f22a16a8/libfreerdp/codec/interleaved.c#L3) +//! - [bitmap.c](https://github.com/FreeRDP/FreeRDP/blob/3a8dce07ea0262b240025bd68b63801578ca63f0/libfreerdp/codec/include/bitmap.c) +use core::fmt; +use core::ops::BitXor; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RlePixelFormat { + Rgb24, + Rgb16, + Rgb15, + Rgb8, +} + +/// Decompresses an RLE compressed bitmap. +/// +/// `src`: source buffer containing compressed bitmap +/// `dst`: destination buffer +/// `width`: decompressed bitmap width +/// `height`: decompressed bitmap height +/// `bpp`: bits per pixel +pub fn decompress( + src: &[u8], + dst: &mut Vec, + width: usize, + height: usize, + bpp: usize, +) -> Result { + match bpp { + Mode24Bpp::BPP => decompress_24_bpp(src, dst, width, height), + Mode16Bpp::BPP => decompress_16_bpp(src, dst, width, height), + Mode15Bpp::BPP => decompress_15_bpp(src, dst, width, height), + Mode8Bpp::BPP => decompress_8_bpp(src, dst, width, height), + invalid => Err(RleError::InvalidBpp { bpp: invalid }), + } +} + +/// Decompresses a 24-bpp RLE compressed bitmap. +/// +/// `src`: source buffer containing compressed bitmap +/// `dst`: destination buffer +/// `width`: decompressed bitmap width +/// `height`: decompressed bitmap height +pub fn decompress_24_bpp( + src: &[u8], + dst: &mut Vec, + width: usize, + height: usize, +) -> Result { + decompress_helper::(src, dst, width, height) +} + +/// Decompresses a 16-bpp RLE compressed bitmap. +/// +/// `src`: source buffer containing compressed bitmap +/// `dst`: destination buffer +/// `width`: decompressed bitmap width +/// `height`: decompressed bitmap height +pub fn decompress_16_bpp( + src: &[u8], + dst: &mut Vec, + width: usize, + height: usize, +) -> Result { + decompress_helper::(src, dst, width, height) +} + +/// Decompresses a 15-bpp RLE compressed bitmap. +/// +/// `src`: source buffer containing compressed bitmap +/// `dst`: destination buffer +/// `width`: decompressed bitmap width +/// `height`: decompressed bitmap height +pub fn decompress_15_bpp( + src: &[u8], + dst: &mut Vec, + width: usize, + height: usize, +) -> Result { + decompress_helper::(src, dst, width, height) +} + +/// Decompresses a 8-bpp RLE compressed bitmap. +/// +/// `src`: source buffer containing compressed bitmap +/// `dst`: destination buffer +/// `width`: decompressed bitmap width +/// `height`: decompressed bitmap height +pub fn decompress_8_bpp( + src: &[u8], + dst: &mut Vec, + width: usize, + height: usize, +) -> Result { + decompress_helper::(src, dst, width, height) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RleError { + InvalidBpp { + bpp: usize, + }, + BadOrderCode, + NotEnoughBytes { + expected: usize, + actual: usize, + }, + InvalidImageSize { + maximum_additional: usize, + required_additional: usize, + }, + EmptyImage, + UnexpectedZeroLength, +} + +impl fmt::Display for RleError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RleError::InvalidBpp { bpp } => write!(f, "invalid bytes per pixel: {bpp}"), + RleError::BadOrderCode => write!(f, "bad RLE order code"), + RleError::NotEnoughBytes { expected, actual } => { + write!(f, "not enough bytes: expected {expected} bytes, but got {actual}") + } + RleError::InvalidImageSize { + maximum_additional, + required_additional, + } => { + write!( + f, + "invalid image size advertised: output buffer can only receive at most {maximum_additional} additional bytes, but {required_additional} bytes are required" + ) + } + RleError::EmptyImage => write!(f, "height or width is zero"), + RleError::UnexpectedZeroLength => write!(f, "unexpected zero-length"), + } + } +} + +fn decompress_helper( + src: &[u8], + dst: &mut Vec, + width: usize, + height: usize, +) -> Result { + if width == 0 || height == 0 { + return Err(RleError::EmptyImage); + } + + let row_delta = Mode::COLOR_DEPTH * width; + dst.resize(row_delta * height, 0); + decompress_impl::(src, dst, row_delta)?; + + Ok(Mode::PIXEL_FORMAT) +} + +macro_rules! ensure_size { + (from: $buf:ident, size: $expected:expr) => {{ + let actual = $buf.remaining_len(); + let expected = $expected; + if expected > actual { + return Err(RleError::NotEnoughBytes { expected, actual }); + } + }}; + (into: $buf:ident, size: $required_additional:expr) => {{ + let maximum_additional = $buf.remaining_len(); + let required_additional = $required_additional; + if required_additional > maximum_additional { + return Err(RleError::InvalidImageSize { + maximum_additional, + required_additional, + }); + } + }}; +} + +/// RLE decompression implementation +/// +/// `src`: source buffer containing compressed bitmap +/// `dst`: destination buffer +/// `row_delta`: scanline length in bytes +fn decompress_impl(src: &[u8], dst: &mut [u8], row_delta: usize) -> Result<(), RleError> { + let mut src = Buf::new(src); + let mut dst = BufMut::new(dst); + + let mut fg_pel = Mode::WHITE_PIXEL; + let mut insert_fg_pel = false; + let mut is_first_line = true; + + while !src.eof() { + // Watch out for the end of the first scanline. + if is_first_line && dst.pos >= row_delta { + is_first_line = false; + insert_fg_pel = false; + } + + ensure_size!(from: src, size: 1); + + let header = src.read_u8(); + + // Extract the compression order code ID from the compression order header. + let code = Code::decode(header); + + // Extract run length + let run_length = code.extract_run_length(header, &mut src)?; + + // Handle Background Run Orders. + if code == Code::REGULAR_BG_RUN || code == Code::MEGA_MEGA_BG_RUN { + ensure_size!(into: dst, size: run_length * Mode::COLOR_DEPTH); + + if is_first_line { + let num_iterations = if insert_fg_pel { + Mode::write_pixel(&mut dst, fg_pel); + run_length - 1 + } else { + run_length + }; + + for _ in 0..num_iterations { + Mode::write_pixel(&mut dst, Mode::BLACK_PIXEL); + } + } else { + let num_iterations = if insert_fg_pel { + let pixel_above = dst.read_pixel_above::(row_delta); + let xored = pixel_above ^ fg_pel; + Mode::write_pixel(&mut dst, xored); + run_length - 1 + } else { + run_length + }; + + for _ in 0..num_iterations { + let pixel_above = dst.read_pixel_above::(row_delta); + Mode::write_pixel(&mut dst, pixel_above); + } + } + + // A follow-on background run order will need a foreground pel inserted. + insert_fg_pel = true; + + continue; + } + + // For any of the other run-types a follow-on background run + // order does not need a foreground pel inserted. + insert_fg_pel = false; + + if code == Code::REGULAR_FG_RUN + || code == Code::MEGA_MEGA_FG_RUN + || code == Code::LITE_SET_FG_FG_RUN + || code == Code::MEGA_MEGA_SET_FG_RUN + { + // Handle Foreground Run Orders. + + ensure_size!(from: src, size: Mode::COLOR_DEPTH); + + if code == Code::LITE_SET_FG_FG_RUN || code == Code::MEGA_MEGA_SET_FG_RUN { + fg_pel = Mode::read_pixel(&mut src); + } + + ensure_size!(into: dst, size: run_length * Mode::COLOR_DEPTH); + + if is_first_line { + for _ in 0..run_length { + Mode::write_pixel(&mut dst, fg_pel); + } + } else { + for _ in 0..run_length { + let pixel_above = dst.read_pixel_above::(row_delta); + let xored = pixel_above ^ fg_pel; + Mode::write_pixel(&mut dst, xored); + } + } + } else if code == Code::LITE_DITHERED_RUN || code == Code::MEGA_MEGA_DITHERED_RUN { + // Handle Dithered Run Orders. + + ensure_size!(from: src, size: 2 * Mode::COLOR_DEPTH); + + let pixel_a = Mode::read_pixel(&mut src); + let pixel_b = Mode::read_pixel(&mut src); + + ensure_size!(into: dst, size: run_length * 2 * Mode::COLOR_DEPTH); + + for _ in 0..run_length { + Mode::write_pixel(&mut dst, pixel_a); + Mode::write_pixel(&mut dst, pixel_b); + } + } else if code == Code::REGULAR_COLOR_RUN || code == Code::MEGA_MEGA_COLOR_RUN { + // Handle Color Run Orders. + + ensure_size!(from: src, size: Mode::COLOR_DEPTH); + + let pixel = Mode::read_pixel(&mut src); + + ensure_size!(into: dst, size: run_length * Mode::COLOR_DEPTH); + + for _ in 0..run_length { + Mode::write_pixel(&mut dst, pixel); + } + } else if code == Code::REGULAR_FGBG_IMAGE + || code == Code::MEGA_MEGA_FGBG_IMAGE + || code == Code::LITE_SET_FG_FGBG_IMAGE + || code == Code::MEGA_MEGA_SET_FGBG_IMAGE + { + // Handle Foreground/Background Image Orders. + + if code == Code::LITE_SET_FG_FGBG_IMAGE || code == Code::MEGA_MEGA_SET_FGBG_IMAGE { + ensure_size!(from: src, size: Mode::COLOR_DEPTH); + fg_pel = Mode::read_pixel(&mut src); + } + + let mut number_to_read = run_length; + + while number_to_read > 0 { + let c_bits = core::cmp::min(8, number_to_read); + + ensure_size!(from: src, size: 1); + let bitmask = src.read_u8(); + + if is_first_line { + write_first_line_fg_bg_image::(&mut dst, bitmask, fg_pel, c_bits)?; + } else { + write_fg_bg_image::(&mut dst, row_delta, bitmask, fg_pel, c_bits)?; + } + + number_to_read -= c_bits; + } + } else if code == Code::REGULAR_COLOR_IMAGE || code == Code::MEGA_MEGA_COLOR_IMAGE { + // Handle Color Image Orders. + + let byte_count = run_length * Mode::COLOR_DEPTH; + + ensure_size!(from: src, size: byte_count); + ensure_size!(into: dst, size: byte_count); + + for _ in 0..byte_count { + dst.write_u8(src.read_u8()); + } + } else if code == Code::SPECIAL_FGBG_1 { + // Handle Special Order 1. + + const MASK_SPECIAL_FG_BG_1: u8 = 0x03; + + if is_first_line { + write_first_line_fg_bg_image::(&mut dst, MASK_SPECIAL_FG_BG_1, fg_pel, 8)?; + } else { + write_fg_bg_image::(&mut dst, row_delta, MASK_SPECIAL_FG_BG_1, fg_pel, 8)?; + } + } else if code == Code::SPECIAL_FGBG_2 { + // Handle Special Order 2. + + const MASK_SPECIAL_FG_BG_2: u8 = 0x05; + + if is_first_line { + write_first_line_fg_bg_image::(&mut dst, MASK_SPECIAL_FG_BG_2, fg_pel, 8)?; + } else { + write_fg_bg_image::(&mut dst, row_delta, MASK_SPECIAL_FG_BG_2, fg_pel, 8)?; + } + } else if code == Code::SPECIAL_WHITE { + // Handle White Order. + + ensure_size!(into: dst, size: Mode::COLOR_DEPTH); + + Mode::write_pixel(&mut dst, Mode::WHITE_PIXEL); + } else if code == Code::SPECIAL_BLACK { + // Handle Black Order. + + ensure_size!(into: dst, size: Mode::COLOR_DEPTH); + + Mode::write_pixel(&mut dst, Mode::BLACK_PIXEL); + } else { + return Err(RleError::BadOrderCode); + } + } + + Ok(()) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +struct Code(u8); + +impl fmt::Debug for Code { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match *self { + Self::REGULAR_BG_RUN => "REGULAR_BG_RUN", + Self::REGULAR_FG_RUN => "REGULAR_FG_RUN", + Self::REGULAR_COLOR_RUN => "REGULAR_COLOR_RUN", + Self::REGULAR_FGBG_IMAGE => "REGULAR_FGBG_IMAGE", + Self::REGULAR_COLOR_IMAGE => "REGULAR_COLOR_IMAGE", + + Self::MEGA_MEGA_BG_RUN => "MEGA_MEGA_BG_RUN", + Self::MEGA_MEGA_FG_RUN => "MEGA_MEGA_FG_RUN", + Self::MEGA_MEGA_SET_FG_RUN => "MEGA_MEGA_SET_FG_RUN", + Self::MEGA_MEGA_DITHERED_RUN => "MEGA_MEGA_DITHERED_RUN", + Self::MEGA_MEGA_COLOR_RUN => "MEGA_MEGA_COLOR_RUN", + Self::MEGA_MEGA_FGBG_IMAGE => "MEGA_MEGA_FGBG_IMAGE", + Self::MEGA_MEGA_SET_FGBG_IMAGE => "MEGA_MEGA_SET_FGBG_IMAGE", + Self::MEGA_MEGA_COLOR_IMAGE => "MEGA_MEGA_COLOR_IMAGE", + + Self::LITE_SET_FG_FG_RUN => "LITE_SET_FG_FG_RUN", + Self::LITE_DITHERED_RUN => "LITE_DITHERED_RUN", + Self::LITE_SET_FG_FGBG_IMAGE => "LITE_SET_FG_FGBG_IMAGE", + + Self::SPECIAL_FGBG_1 => "SPECIAL_FGBG_1", + Self::SPECIAL_FGBG_2 => "SPECIAL_FGBG_2", + Self::SPECIAL_WHITE => "SPECIAL_WHITE", + Self::SPECIAL_BLACK => "SPECIAL_BLACK", + + _ => "UNKNOWN", + }; + + write!(f, "Code(0x{:02X}-{name})", self.0) + } +} + +impl Code { + const REGULAR_BG_RUN: Code = Code(0x00); + const REGULAR_FG_RUN: Code = Code(0x01); + const REGULAR_COLOR_RUN: Code = Code(0x03); + const REGULAR_FGBG_IMAGE: Code = Code(0x02); + const REGULAR_COLOR_IMAGE: Code = Code(0x04); + + const MEGA_MEGA_BG_RUN: Code = Code(0xF0); + const MEGA_MEGA_FG_RUN: Code = Code(0xF1); + const MEGA_MEGA_SET_FG_RUN: Code = Code(0xF6); + const MEGA_MEGA_DITHERED_RUN: Code = Code(0xF8); + const MEGA_MEGA_COLOR_RUN: Code = Code(0xF3); + const MEGA_MEGA_FGBG_IMAGE: Code = Code(0xF2); + const MEGA_MEGA_SET_FGBG_IMAGE: Code = Code(0xF7); + const MEGA_MEGA_COLOR_IMAGE: Code = Code(0xF4); + + const LITE_SET_FG_FG_RUN: Code = Code(0x0C); + const LITE_DITHERED_RUN: Code = Code(0x0E); + const LITE_SET_FG_FGBG_IMAGE: Code = Code(0x0D); + + const SPECIAL_FGBG_1: Code = Code(0xF9); + const SPECIAL_FGBG_2: Code = Code(0xFA); + const SPECIAL_WHITE: Code = Code(0xFD); + const SPECIAL_BLACK: Code = Code(0xFE); + + fn decode(header: u8) -> Self { + if (header & 0xC0) != 0xC0 { + // REGULAR orders + // (000x xxxx, 001x xxxx, 010x xxxx, 011x xxxx, 100x xxxx) + Code(header >> 5) + } else if (header & 0xF0) == 0xF0 { + // MEGA and SPECIAL orders (0xF*) + Code(header) + } else { + // LITE orders + // (1100 xxxx, 1101 xxxx, 1110 xxxx) + Code(header >> 4) + } + } + + /// Extract the run length of a compression order. + fn extract_run_length(self, header: u8, src: &mut Buf<'_>) -> Result { + match self { + Self::REGULAR_FGBG_IMAGE => extract_run_length_fg_bg(header, MASK_REGULAR_RUN_LENGTH, src), + + Self::LITE_SET_FG_FGBG_IMAGE => extract_run_length_fg_bg(header, MASK_LITE_RUN_LENGTH, src), + + Self::REGULAR_BG_RUN | Self::REGULAR_FG_RUN | Self::REGULAR_COLOR_RUN | Self::REGULAR_COLOR_IMAGE => { + extract_run_length_regular(header, src) + } + + Self::LITE_SET_FG_FG_RUN | Self::LITE_DITHERED_RUN => extract_run_length_lite(header, src), + + Self::MEGA_MEGA_BG_RUN + | Self::MEGA_MEGA_FG_RUN + | Self::MEGA_MEGA_SET_FG_RUN + | Self::MEGA_MEGA_DITHERED_RUN + | Self::MEGA_MEGA_COLOR_RUN + | Self::MEGA_MEGA_FGBG_IMAGE + | Self::MEGA_MEGA_SET_FGBG_IMAGE + | Self::MEGA_MEGA_COLOR_IMAGE => extract_run_length_mega_mega(src), + + Self::SPECIAL_FGBG_1 | Self::SPECIAL_FGBG_2 | Self::SPECIAL_WHITE | Self::SPECIAL_BLACK => Ok(0), + + _ => Ok(0), + } + } +} + +const MASK_REGULAR_RUN_LENGTH: u8 = 0x1F; +const MASK_LITE_RUN_LENGTH: u8 = 0x0F; + +/// Extract the run length of a Foreground/Background Image Order. +fn extract_run_length_fg_bg(header: u8, length_mask: u8, src: &mut Buf<'_>) -> Result { + match header & length_mask { + 0 => { + ensure_size!(from: src, size: 1); + Ok(usize::from(src.read_u8()) + 1) + } + run_length => Ok(usize::from(run_length) * 8), + } +} + +/// Extract the run length of a regular-form compression order. +fn extract_run_length_regular(header: u8, src: &mut Buf<'_>) -> Result { + match header & MASK_REGULAR_RUN_LENGTH { + 0 => { + // An extended (MEGA) run. + ensure_size!(from: src, size: 1); + Ok(usize::from(src.read_u8()) + 32) + } + run_length => Ok(usize::from(run_length)), + } +} + +fn extract_run_length_lite(header: u8, src: &mut Buf<'_>) -> Result { + match header & MASK_LITE_RUN_LENGTH { + 0 => { + // An extended (MEGA) run. + ensure_size!(from: src, size: 1); + Ok(usize::from(src.read_u8()) + 16) + } + run_length => Ok(usize::from(run_length)), + } +} + +fn extract_run_length_mega_mega(src: &mut Buf<'_>) -> Result { + ensure_size!(from: src, size: 2); + + let run_length = usize::from(src.read_u16()); + + if run_length == 0 { + Err(RleError::UnexpectedZeroLength) + } else { + Ok(run_length) + } +} + +// TODO: use ironrdp_core::ReadCursor instead +struct Buf<'a> { + inner: &'a [u8], + pos: usize, +} + +impl<'a> Buf<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { inner: bytes, pos: 0 } + } + + fn remaining_len(&self) -> usize { + self.inner.len() - self.pos + } + + fn read(&mut self) -> [u8; N] { + let bytes = &self.inner[self.pos..self.pos + N]; + self.pos += N; + bytes.try_into().expect("N-elements array") + } + + fn read_u8(&mut self) -> u8 { + u8::from_le_bytes(self.read::<1>()) + } + + fn read_u16(&mut self) -> u16 { + u16::from_le_bytes(self.read::<2>()) + } + + fn read_u24(&mut self) -> u32 { + let bytes = self.read::<3>(); + u32::from_le_bytes([bytes[0], bytes[1], bytes[2], 0]) + } + + fn rewinded(&'a self, len: usize) -> Buf<'a> { + Buf { + inner: self.inner, + pos: self.pos - len, + } + } + + fn eof(&self) -> bool { + self.pos == self.inner.len() + } +} + +// TODO: use ironrdp_core::WriteCursor instead +struct BufMut<'a> { + inner: &'a mut [u8], + pos: usize, +} + +impl<'a> BufMut<'a> { + fn new(bytes: &'a mut [u8]) -> Self { + Self { inner: bytes, pos: 0 } + } + + fn remaining_len(&self) -> usize { + self.inner.len() - self.pos + } + + fn write(&mut self, bytes: &[u8]) { + self.inner[self.pos..self.pos + bytes.len()].copy_from_slice(bytes); + self.pos += bytes.len(); + } + + fn write_u8(&mut self, value: u8) { + self.write(&[value]); + } + + fn write_u16(&mut self, value: u16) { + self.write(&value.to_le_bytes()); + } + + fn write_u24(&mut self, value: u32) { + self.write(&value.to_le_bytes()[..3]); + } + + fn read_pixel_above(&self, row_delta: usize) -> Mode::Pixel { + let read_buf = Buf { + inner: self.inner, + pos: self.pos, + }; + let mut read_buf = read_buf.rewinded(row_delta); + Mode::read_pixel(&mut read_buf) + } +} + +trait DepthMode { + type Pixel: Copy + BitXor; + + /// The color depth (in bytes per pixel) for this mode + const COLOR_DEPTH: usize; + + /// Bits per pixel + const BPP: usize; + + /// Pixel format for this depth mode + const PIXEL_FORMAT: RlePixelFormat; + + /// The black pixel value + const BLACK_PIXEL: Self::Pixel; + + /// The white pixel value + const WHITE_PIXEL: Self::Pixel; + + /// Writes a pixel to the specified buffer + fn write_pixel(dst: &mut BufMut<'_>, pixel: Self::Pixel); + + /// Reads a pixel from the specified buffer + fn read_pixel(src: &mut Buf<'_>) -> Self::Pixel; +} + +struct Mode8Bpp; + +impl DepthMode for Mode8Bpp { + type Pixel = u8; + + const COLOR_DEPTH: usize = 1; + + const BPP: usize = 8; + + const PIXEL_FORMAT: RlePixelFormat = RlePixelFormat::Rgb8; + + const BLACK_PIXEL: Self::Pixel = 0x00; + + const WHITE_PIXEL: Self::Pixel = 0xFF; + + fn write_pixel(dst: &mut BufMut<'_>, pixel: Self::Pixel) { + dst.write_u8(pixel); + } + + fn read_pixel(src: &mut Buf<'_>) -> Self::Pixel { + src.read_u8() + } +} + +struct Mode15Bpp; + +impl DepthMode for Mode15Bpp { + type Pixel = u16; + + const COLOR_DEPTH: usize = 2; + + const BPP: usize = 15; + + const PIXEL_FORMAT: RlePixelFormat = RlePixelFormat::Rgb15; + + const BLACK_PIXEL: Self::Pixel = 0x0000; + + // 5 bits per RGB component: + // 0111 1111 1111 1111 (binary) + const WHITE_PIXEL: Self::Pixel = 0x7FFF; + + fn write_pixel(dst: &mut BufMut<'_>, pixel: Self::Pixel) { + dst.write_u16(pixel); + } + + fn read_pixel(src: &mut Buf<'_>) -> Self::Pixel { + src.read_u16() + } +} + +struct Mode16Bpp; + +impl DepthMode for Mode16Bpp { + type Pixel = u16; + + const COLOR_DEPTH: usize = 2; + + const BPP: usize = 16; + + const PIXEL_FORMAT: RlePixelFormat = RlePixelFormat::Rgb16; + + const BLACK_PIXEL: Self::Pixel = 0x0000; + + // 5 bits for red, 6 bits for green, 5 bits for green: + // 1111 1111 1111 1111 (binary) + const WHITE_PIXEL: Self::Pixel = 0xFFFF; + + fn write_pixel(dst: &mut BufMut<'_>, pixel: Self::Pixel) { + dst.write_u16(pixel); + } + + fn read_pixel(src: &mut Buf<'_>) -> Self::Pixel { + src.read_u16() + } +} + +struct Mode24Bpp; + +impl DepthMode for Mode24Bpp { + type Pixel = u32; + + const COLOR_DEPTH: usize = 3; + + const BPP: usize = 24; + + const PIXEL_FORMAT: RlePixelFormat = RlePixelFormat::Rgb24; + + const BLACK_PIXEL: Self::Pixel = 0x00_0000; + + // 8 bits per RGB component: + // 1111 1111 1111 1111 1111 1111 (binary) + const WHITE_PIXEL: Self::Pixel = 0xFF_FFFF; + + fn write_pixel(dst: &mut BufMut<'_>, pixel: Self::Pixel) { + dst.write_u24(pixel); + } + + fn read_pixel(src: &mut Buf<'_>) -> Self::Pixel { + src.read_u24() + } +} + +/// Writes a foreground/background image to a destination buffer. +fn write_fg_bg_image( + dst: &mut BufMut<'_>, + row_delta: usize, + bitmask: u8, + fg_pel: Mode::Pixel, + mut c_bits: usize, +) -> Result<(), RleError> { + ensure_size!(into: dst, size: c_bits * Mode::COLOR_DEPTH); + + let mut mask = 0x01; + + repeat::<8>(|| { + let above_pixel = dst.read_pixel_above::(row_delta); + + if bitmask & mask != 0 { + Mode::write_pixel(dst, above_pixel ^ fg_pel); + } else { + Mode::write_pixel(dst, above_pixel); + } + + c_bits -= 1; + mask <<= 1; + + c_bits == 0 + }); + + Ok(()) +} + +/// Writes a foreground/background image to a destination buffer +fn write_first_line_fg_bg_image( + dst: &mut BufMut<'_>, + bitmask: u8, + fg_pel: Mode::Pixel, + mut c_bits: usize, +) -> Result<(), RleError> { + ensure_size!(into: dst, size: c_bits * Mode::COLOR_DEPTH); + + let mut mask = 0x01; + + repeat::<8>(|| { + if bitmask & mask != 0 { + Mode::write_pixel(dst, fg_pel); + } else { + Mode::write_pixel(dst, Mode::BLACK_PIXEL); + } + + c_bits -= 1; + mask <<= 1; + + c_bits == 0 + }); + + Ok(()) +} + +fn repeat(mut op: impl FnMut() -> bool) { + for _ in 0..N { + let stop = op(); + + if stop { + return; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! test_buf_mut { + ($mode:ident) => {{ + let row_delta = 4 * $mode::COLOR_DEPTH; + let mut buf = vec![0; row_delta * 2]; + let mut dst = BufMut::new(buf.as_mut_slice()); + + $mode::write_pixel(&mut dst, 0xDEAD); + $mode::write_pixel(&mut dst, 0xBEEF); + $mode::write_pixel(&mut dst, 0xFADE); + $mode::write_pixel(&mut dst, 0xFEED); + + assert_eq!(dst.read_pixel_above::<$mode>(row_delta), 0xDEAD); + $mode::write_pixel(&mut dst, $mode::WHITE_PIXEL); + assert_eq!(dst.read_pixel_above::<$mode>(row_delta), 0xBEEF); + $mode::write_pixel(&mut dst, $mode::WHITE_PIXEL); + assert_eq!(dst.read_pixel_above::<$mode>(row_delta), 0xFADE); + $mode::write_pixel(&mut dst, $mode::WHITE_PIXEL); + assert_eq!(dst.read_pixel_above::<$mode>(row_delta), 0xFEED); + $mode::write_pixel(&mut dst, $mode::WHITE_PIXEL); + }}; + } + + #[test] + fn buf_mut_16_bpp() { + test_buf_mut!(Mode16Bpp); + } + + #[test] + fn buf_mut_15_bpp() { + test_buf_mut!(Mode15Bpp); + } + + #[test] + fn buf_mut_24_bpp() { + test_buf_mut!(Mode24Bpp); + } +} diff --git a/crates/ironrdp-graphics/src/rlgr.rs b/crates/ironrdp-graphics/src/rlgr.rs new file mode 100644 index 00000000..3fc33ffa --- /dev/null +++ b/crates/ironrdp-graphics/src/rlgr.rs @@ -0,0 +1,429 @@ +use core::cmp::min; +use std::io; + +use bitvec::field::BitField as _; +use bitvec::prelude::*; +use ironrdp_pdu::codecs::rfx::EntropyAlgorithm; +use yuv::YuvError; + +use crate::utils::Bits; + +const KP_MAX: u32 = 80; +const LS_GR: u32 = 3; +const UP_GR: u32 = 4; +const DN_GR: u32 = 6; +const UQ_GR: u32 = 3; +const DQ_GR: u32 = 3; + +macro_rules! write_byte { + ($output:ident, $value:ident) => { + if !$output.is_empty() { + $output[0] = $value; + $output = &mut $output[1..]; + } else { + break; + } + }; +} + +macro_rules! try_split_bits { + ($bits:ident, $n:expr) => { + if $bits.len() < $n { + break; + } else { + $bits.split_to($n) + } + }; +} + +struct BitStream<'a> { + bits: &'a mut BitSlice, + idx: usize, +} + +impl<'a> BitStream<'a> { + fn new(slice: &'a mut [u8]) -> Self { + let bits = slice.view_bits_mut::(); + Self { bits, idx: 0 } + } + + fn output_bit(&mut self, count: usize, val: bool) { + self.bits[self.idx..self.idx + count].fill(val); + self.idx += count; + } + + fn output_bits(&mut self, num_bits: usize, val: u32) { + self.bits[self.idx..self.idx + num_bits].store_be(val); + self.idx += num_bits; + } + + fn len(&self) -> usize { + self.idx.div_ceil(8) + } +} + +pub fn encode(mode: EntropyAlgorithm, input: &[i16], tile: &mut [u8]) -> Result { + #![expect( + clippy::as_conversions, + reason = "u32-to-usize and usize-to-u32 conversions, mostly fine, and hot loop" + )] + + if input.is_empty() { + return Err(RlgrError::EmptyTile); + } + + let mut k: u32 = 1; + let kr: u32 = 1; + let mut kp: u32 = k << LS_GR; + let mut krp: u32 = kr << LS_GR; + let mut bits = BitStream::new(tile); + + let mut input = input.iter().peekable(); + + while input.peek().is_some() { + match CompressionMode::from(k) { + CompressionMode::RunLength => { + let mut nz = 0; + while let Some(&&x) = input.peek() { + if x == 0 { + nz += 1; + input.next(); + } else { + break; + } + } + let mut runmax: u32 = 1 << k; + while nz >= runmax { + bits.output_bit(1, false); + nz -= runmax; + kp = min(kp + UP_GR, KP_MAX); + k = kp >> LS_GR; + runmax = 1 << k; + } + bits.output_bit(1, true); + bits.output_bits(k as usize, nz); + + if let Some(val) = input.next() { + let mag = u32::from(val.unsigned_abs()); + bits.output_bit(1, *val < 0); + code_gr(&mut bits, &mut krp, mag - 1); + } + kp = kp.saturating_sub(DN_GR); + k = kp >> LS_GR; + } + CompressionMode::GolombRice => { + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (prior check)")] + let input_first = *input + .next() + .expect("value is guaranteed to be `Some` due to the prior check"); + + match mode { + EntropyAlgorithm::Rlgr1 => { + let two_ms = get_2magsign(input_first); + code_gr(&mut bits, &mut krp, two_ms); + if two_ms == 0 { + kp = min(kp + UP_GR, KP_MAX); + } else { + kp = kp.saturating_sub(DQ_GR); + } + k = kp >> LS_GR; + } + EntropyAlgorithm::Rlgr3 => { + let two_ms1 = get_2magsign(input_first); + let two_ms2 = input.next().map(|&n| get_2magsign(n)).unwrap_or(1); + let sum2ms = two_ms1 + two_ms2; + code_gr(&mut bits, &mut krp, sum2ms); + + let m = 32 - sum2ms.leading_zeros() as usize; + if m != 0 { + bits.output_bits(m, two_ms1); + } + + if two_ms1 != 0 && two_ms2 != 0 { + kp = kp.saturating_sub(2 * DQ_GR); + k = kp >> LS_GR; + } else if two_ms1 == 0 && two_ms2 == 0 { + kp = min(kp + 2 * UQ_GR, KP_MAX); + k = kp >> LS_GR; + } + } + } + } + } + } + + Ok(bits.len()) +} + +fn get_2magsign(val: i16) -> u32 { + let sign = if val < 0 { 1 } else { 0 }; + + (u32::from(val.unsigned_abs())) * 2 - sign +} + +fn code_gr(bits: &mut BitStream<'_>, krp: &mut u32, val: u32) { + #![expect( + clippy::as_conversions, + reason = "u32-to-usize and usize-to-u32 conversions, mostly fine, and hot loop" + )] + + let kr = (*krp >> LS_GR) as usize; + + let vk = val >> kr; + let vk_usize = vk as usize; + + bits.output_bit(vk_usize, true); + bits.output_bit(1, false); + + if kr != 0 { + let remainder = val & ((1 << kr) - 1); + bits.output_bits(kr, remainder); + } + + if vk == 0 { + *krp = krp.saturating_sub(2); + } else if vk > 1 { + *krp = min(*krp + vk, KP_MAX); + } +} + +pub fn decode(mode: EntropyAlgorithm, tile: &[u8], mut output: &mut [i16]) -> Result<(), RlgrError> { + #![expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "u32-to-usize and usize-to-u32 conversions, mostly fine, and hot loop" + )] + + if tile.is_empty() { + return Err(RlgrError::EmptyTile); + } + + let mut k: u32 = 1; + let mut kr: u32 = 1; + let mut kp: u32 = k << LS_GR; + let mut krp: u32 = kr << LS_GR; + + let mut bits = Bits::new(BitSlice::from_slice(tile)); + + while !bits.is_empty() && !output.is_empty() { + match CompressionMode::from(k) { + CompressionMode::RunLength => { + let number_of_zeros = truncate_leading_value(&mut bits, false); + try_split_bits!(bits, 1); + let run = count_run(number_of_zeros, &mut k, &mut kp) + load_be_u32(try_split_bits!(bits, k as usize)); + + let sign_bit = try_split_bits!(bits, 1).load_be::(); + + let number_of_ones = truncate_leading_value(&mut bits, true); + try_split_bits!(bits, 1); + + let code_remainder = load_be_u32(try_split_bits!(bits, kr as usize)) + ((number_of_ones as u32) << kr); + + update_parameters_according_to_number_of_ones(number_of_ones, &mut kr, &mut krp); + kp = kp.saturating_sub(DN_GR); + k = kp >> LS_GR; + + let magnitude = compute_rl_magnitude(sign_bit, code_remainder)?; + + let size = min(run as usize, output.len()); + fill(&mut output[..size], 0); + output = &mut output[size..]; + write_byte!(output, magnitude); + } + CompressionMode::GolombRice => { + let number_of_ones = truncate_leading_value(&mut bits, true); + try_split_bits!(bits, 1); + + let code_remainder = load_be_u32(try_split_bits!(bits, kr as usize)) + ((number_of_ones as u32) << kr); + + update_parameters_according_to_number_of_ones(number_of_ones, &mut kr, &mut krp); + + match mode { + EntropyAlgorithm::Rlgr1 => { + let magnitude = compute_rlgr1_magnitude(code_remainder, &mut k, &mut kp)?; + write_byte!(output, magnitude); + } + EntropyAlgorithm::Rlgr3 => { + let n_index = compute_n_index(code_remainder); + + let val1 = load_be_u32(try_split_bits!(bits, n_index)); + let val2 = code_remainder - val1; + if val1 != 0 && val2 != 0 { + kp = kp.saturating_sub(2 * DQ_GR); + k = kp >> LS_GR; + } else if val1 == 0 && val2 == 0 { + kp = min(kp + 2 * UQ_GR, KP_MAX); + k = kp >> LS_GR; + } + + let magnitude = compute_rlgr3_magnitude(val1)?; + write_byte!(output, magnitude); + + let magnitude = compute_rlgr3_magnitude(val2)?; + write_byte!(output, magnitude); + } + } + } + } + } + + // Fill remaining buffer with zeros. + fill(output, 0); + + Ok(()) +} + +fn fill(buffer: &mut [i16], value: i16) { + for v in buffer { + *v = value; + } +} + +fn load_be_u32(s: &BitSlice) -> u32 { + if s.is_empty() { + 0 + } else { + s.load_be::() + } +} + +// Returns number of truncated bits +fn truncate_leading_value(bits: &mut Bits<'_>, value: bool) -> usize { + let leading_values = if value { + bits.leading_ones() + } else { + bits.leading_zeros() + }; + bits.split_to(leading_values); + leading_values +} + +fn count_run(number_of_zeros: usize, k: &mut u32, kp: &mut u32) -> u32 { + core::iter::repeat_with(|| { + let run = 1 << *k; + *kp = min(*kp + UP_GR, KP_MAX); + *k = *kp >> LS_GR; + + run + }) + .take(number_of_zeros) + .sum() +} + +fn compute_rl_magnitude(sign_bit: u8, code_remainder: u32) -> Result { + let rl_magnitude = + i16::try_from(code_remainder + 1).map_err(|_| RlgrError::InvalidIntegralConversion("code remainder + 1"))?; + + if sign_bit != 0 { + Ok(-rl_magnitude) + } else { + Ok(rl_magnitude) + } +} + +fn compute_rlgr1_magnitude(code_remainder: u32, k: &mut u32, kp: &mut u32) -> Result { + if code_remainder == 0 { + *kp = min(*kp + UQ_GR, KP_MAX); + *k = *kp >> LS_GR; + + Ok(0) + } else { + *kp = kp.saturating_sub(DQ_GR); + *k = *kp >> LS_GR; + + if code_remainder % 2 != 0 { + Ok(-i16::try_from((code_remainder + 1) >> 1) + .map_err(|_| RlgrError::InvalidIntegralConversion("(code remainder + 1) >> 1"))?) + } else { + i16::try_from(code_remainder >> 1).map_err(|_| RlgrError::InvalidIntegralConversion("code remainder >> 1")) + } + } +} + +fn compute_rlgr3_magnitude(val: u32) -> Result { + if val % 2 != 0 { + Ok(-i16::try_from((val + 1) >> 1).map_err(|_| RlgrError::InvalidIntegralConversion("(val + 1) >> 1"))?) + } else { + i16::try_from(val >> 1).map_err(|_| RlgrError::InvalidIntegralConversion("val >> 1")) + } +} + +fn compute_n_index(code_remainder: u32) -> usize { + if code_remainder == 0 { + return 0; + } + + let code_bytes = code_remainder.to_be_bytes(); + let code_bits = BitSlice::::from_slice(code_bytes.as_ref()); + let leading_zeros = code_bits.leading_zeros(); + + 32 - leading_zeros +} + +fn update_parameters_according_to_number_of_ones(number_of_ones: usize, kr: &mut u32, krp: &mut u32) { + #![expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "usize-to-u32 conversions, hot loop" + )] + + if number_of_ones == 0 { + *krp = (*krp).saturating_sub(2); + *kr = *krp >> LS_GR; + } else if number_of_ones > 1 { + *krp = min(*krp + (number_of_ones as u32), KP_MAX); + *kr = *krp >> LS_GR; + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +enum CompressionMode { + RunLength, + GolombRice, +} + +impl From for CompressionMode { + fn from(m: u32) -> Self { + if m != 0 { + Self::RunLength + } else { + Self::GolombRice + } + } +} + +#[derive(Debug)] +pub enum RlgrError { + Io(io::Error), + Yuv(YuvError), + EmptyTile, + InvalidIntegralConversion(&'static str), +} + +impl core::fmt::Display for RlgrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Io(_) => write!(f, "IO error"), + Self::Yuv(_) => write!(f, "YUV error"), + Self::EmptyTile => write!(f, "the input tile is empty"), + Self::InvalidIntegralConversion(s) => write!(f, "invalid `{s}`: out of range integral type conversion"), + } + } +} + +impl core::error::Error for RlgrError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Io(error) => Some(error), + Self::Yuv(error) => Some(error), + Self::EmptyTile => None, + Self::InvalidIntegralConversion(_) => None, + } + } +} + +impl From for RlgrError { + fn from(err: io::Error) -> Self { + Self::Io(err) + } +} diff --git a/crates/ironrdp-graphics/src/subband_reconstruction.rs b/crates/ironrdp-graphics/src/subband_reconstruction.rs new file mode 100644 index 00000000..d6206d5f --- /dev/null +++ b/crates/ironrdp-graphics/src/subband_reconstruction.rs @@ -0,0 +1,108 @@ +pub fn decode(buffer: &mut [i16]) { + for i in 1..buffer.len() { + buffer[i] = buffer[i].overflowing_add(buffer[i - 1]).0; + } +} + +pub fn encode(buffer: &mut [i16]) { + if buffer.is_empty() { + return; + } + let mut prev = buffer[0]; + for buf in buffer.iter_mut().skip(1) { + let b = *buf; + *buf = b.overflowing_sub(prev).0; + prev = b; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_does_not_panic_for_empty_buffer() { + let mut buffer = []; + decode(&mut buffer); + assert!(buffer.is_empty()); + } + + #[test] + fn decode_does_not_change_buffer_with_one_element() { + let mut buffer = [1]; + decode(&mut buffer); + assert_eq!([1], buffer); + } + + #[test] + fn decode_changes_last_element_for_buffer_with_two_elements() { + let mut buffer = [1, 2]; + let expected = [1, 3]; + decode(&mut buffer); + assert_eq!(expected, buffer); + } + + #[test] + fn decode_changes_last_element_for_buffer_with_min_elements() { + let mut buffer = [-32768, -32768, -32768, -32768, -32768]; + let expected = [-32768, 0, -32768, 0, -32768]; + decode(&mut buffer); + assert_eq!(expected, buffer); + } + + #[test] + fn encode_changes_last_element_for_buffer_with_min_elements() { + let mut buffer = [-32768, 0, -32768, 0, -32768]; + let expected = [-32768, -32768, -32768, -32768, -32768]; + encode(&mut buffer); + assert_eq!(expected, buffer); + } + + #[test] + fn decode_changes_last_element_for_buffer_with_max_elements() { + let mut buffer = [32767, 32767, 32767, 32767, 32767]; + let expected = [32767, -2, 32765, -4, 32763]; + decode(&mut buffer); + assert_eq!(expected, buffer); + } + + #[test] + fn encode_changes_last_element_for_buffer_with_max_elements() { + let mut buffer = [32767, -2, 32765, -4, 32763]; + let expected = [32767, 32767, 32767, 32767, 32767]; + encode(&mut buffer); + assert_eq!(expected, buffer); + } + + #[test] + fn decode_does_not_change_zeroed_buffer() { + let mut buffer = [0, 0, 0, 0, 0]; + let expected = [0, 0, 0, 0, 0]; + decode(&mut buffer); + assert_eq!(expected, buffer); + } + + #[test] + fn encode_does_not_change_zeroed_buffer() { + let mut buffer = [0, 0, 0, 0, 0]; + let expected = [0, 0, 0, 0, 0]; + encode(&mut buffer); + assert_eq!(expected, buffer); + } + + #[test] + fn decode_works_with_five_not_zeroed_elements() { + let mut buffer = [1, 2, 3, 4, 5]; + let expected = [1, 3, 6, 10, 15]; + decode(&mut buffer); + assert_eq!(expected, buffer); + } + + #[test] + fn encode_works_with_five_not_zeroed_elements() { + let mut buffer = [1, 3, 6, 10, 15]; + let expected = [1, 2, 3, 4, 5]; + encode(&mut buffer); + assert_eq!(expected, buffer); + } +} diff --git a/crates/ironrdp-graphics/src/utils.rs b/crates/ironrdp-graphics/src/utils.rs new file mode 100644 index 00000000..a00446dd --- /dev/null +++ b/crates/ironrdp-graphics/src/utils.rs @@ -0,0 +1,38 @@ +use core::ops; + +use bitvec::prelude::{BitSlice, Msb0}; + +// FIXME: check if this should be deleted in favor of something else + +pub(crate) struct Bits<'a> { + bits_slice: &'a BitSlice, + remaining_bits_of_last_byte: usize, +} + +impl<'a> Bits<'a> { + pub(crate) fn new(bits_slice: &'a BitSlice) -> Self { + Self { + bits_slice, + remaining_bits_of_last_byte: 0, + } + } + + pub(crate) fn split_to(&mut self, at: usize) -> &'a BitSlice { + let (value, new_bits) = self.bits_slice.split_at(at); + self.bits_slice = new_bits; + self.remaining_bits_of_last_byte = (self.remaining_bits_of_last_byte + at) % 8; + value + } + + pub(crate) fn remaining_bits_of_last_byte(&self) -> usize { + self.remaining_bits_of_last_byte + } +} + +impl ops::Deref for Bits<'_> { + type Target = BitSlice; + + fn deref(&self) -> &Self::Target { + self.bits_slice + } +} diff --git a/crates/ironrdp-graphics/src/zgfx/circular_buffer.rs b/crates/ironrdp-graphics/src/zgfx/circular_buffer.rs new file mode 100644 index 00000000..99469da8 --- /dev/null +++ b/crates/ironrdp-graphics/src/zgfx/circular_buffer.rs @@ -0,0 +1,217 @@ +use core::cmp::min; +use std::io; + +pub(crate) struct FixedCircularBuffer { + buffer: Vec, + position: usize, +} + +impl FixedCircularBuffer { + pub(crate) fn new(size: usize) -> Self { + Self { + buffer: vec![0; size], + position: 0, + } + } + + pub(crate) fn read_with_offset(&self, offset: usize, length: usize, output: &mut impl io::Write) -> io::Result<()> { + let position = (self.buffer.len() + self.position - offset) % self.buffer.len(); + + // will take the offset if the destination length is greater than the offset, + // i.e. greater than the current buffer position. + let dst_length = min(offset, length); + let mut written = 0; + + if position + dst_length <= self.buffer.len() { + while written < length { + let to_write = min(length - written, dst_length); + output.write_all(&self.buffer[position..position + to_write])?; + written += to_write; + } + } else { + let to_front = &self.buffer[position..]; + let to_back = &self.buffer[..dst_length - to_front.len()]; + + while written < length { + let to_write = min(length - written, dst_length); + + let to_write_to_front = min(to_front.len(), to_write); + output.write_all(&to_front[..to_write_to_front])?; + output.write_all(&to_back[..to_write - to_write_to_front])?; + + written += to_write; + } + } + + Ok(()) + } +} + +impl io::Write for FixedCircularBuffer { + fn write(&mut self, mut buf: &[u8]) -> io::Result { + let bytes_written = buf.len(); + + if buf.len() > self.buffer.len() { + let residue = buf.len() - self.buffer.len(); + buf = &buf[residue..]; + self.position = (self.position + residue) % self.buffer.len(); + } + + if self.position + buf.len() <= self.buffer.len() { + self.buffer[self.position..self.position + buf.len()].clone_from_slice(buf); + + self.position += buf.len(); + } else { + let (to_back, to_front) = buf.split_at(self.buffer.len() - self.position); + self.buffer[self.position..].clone_from_slice(to_back); + self.buffer[0..to_front.len()].clone_from_slice(to_front); + + self.position = buf.len() - to_back.len(); + } + + if self.position == self.buffer.len() { + self.position = 0; + } + + Ok(bytes_written) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::io::Write as _; + + use super::*; + + #[test] + fn fixed_circular_buffer_correctly_writes_buffer_less_then_internal_buffer_size() { + let size = 8; + let mut circular_buffer = FixedCircularBuffer::new(size); + let to_write = [1, 2, 3]; + + circular_buffer.write_all(to_write.as_ref()).unwrap(); + + assert_eq!(vec![1, 2, 3, 0, 0, 0, 0, 0], circular_buffer.buffer); + assert_eq!(to_write.len(), circular_buffer.position); + } + + #[test] + fn fixed_circular_buffer_correctly_writes_buffer_less_then_internal_buffer_size_to_end() { + let size = 8; + let mut circular_buffer = FixedCircularBuffer::new(size); + circular_buffer.position = 5; + let to_write = [1, 2, 3]; + + circular_buffer.write_all(to_write.as_ref()).unwrap(); + + assert_eq!(vec![0, 0, 0, 0, 0, 1, 2, 3], circular_buffer.buffer); + assert_eq!(0, circular_buffer.position); + } + + #[test] + fn fixed_circular_buffer_correctly_writes_buffer_bigger_then_position_with_remaining_size() { + let size = 8; + let mut circular_buffer = FixedCircularBuffer::new(size); + circular_buffer.position = 6; + let to_write = [1, 2, 3]; + + circular_buffer.write_all(to_write.as_ref()).unwrap(); + + assert_eq!(vec![3, 0, 0, 0, 0, 0, 1, 2], circular_buffer.buffer); + assert_eq!(1, circular_buffer.position); + } + + #[test] + fn fixed_circular_buffer_correctly_writes_buffer_bigger_then_internal_buffer_size() { + let size = 8; + let mut circular_buffer = FixedCircularBuffer::new(size); + let to_write = (1..=10).collect::>(); + + circular_buffer.write_all(to_write.as_ref()).unwrap(); + + assert_eq!(vec![9, 10, 3, 4, 5, 6, 7, 8], circular_buffer.buffer); + assert_eq!(2, circular_buffer.position); + } + + #[test] + fn fixed_circular_buffer_correctly_writes_buffer_bigger_then_internal_buffer_size_with_position_at_end() { + let size = 8; + let mut circular_buffer = FixedCircularBuffer::new(size); + circular_buffer.position = 6; + let to_write = (1..=10).collect::>(); + + circular_buffer.write_all(to_write.as_ref()).unwrap(); + + assert_eq!(vec![3, 4, 5, 6, 7, 8, 9, 10], circular_buffer.buffer); + assert_eq!(0, circular_buffer.position); + } + + #[test] + fn fixed_circular_buffer_correctly_reads_buffer_with_length_not_greater_then_buffer_length() { + let circular_buffer = FixedCircularBuffer { + buffer: vec![11, 12, 13, 14, 15, 16, 7, 8, 9, 10], + position: 6, + }; + let expected = vec![11, 12, 13, 14]; + + let mut output = Vec::with_capacity(expected.len()); + circular_buffer.read_with_offset(6, 4, &mut output).unwrap(); + assert_eq!(expected, output); + } + + #[test] + fn fixed_circular_buffer_correctly_reads_buffer_from_end_to_start() { + let circular_buffer = FixedCircularBuffer { + buffer: vec![11, 12, 13, 14, 15, 16, 7, 8, 9, 10], + position: 6, + }; + let expected = vec![8, 9, 10, 11, 12, 13, 14]; + + let mut output = Vec::with_capacity(expected.len()); + circular_buffer.read_with_offset(9, 7, &mut output).unwrap(); + assert_eq!(expected, output); + } + + #[test] + fn fixed_circular_buffer_correctly_reads_buffer_with_repeating_one_byte() { + let circular_buffer = FixedCircularBuffer { + buffer: vec![11, 12, 13, 14, 15, 16, 7, 8, 9, 10], + position: 6, + }; + let expected = vec![16; 7]; + + let mut output = Vec::with_capacity(expected.len()); + circular_buffer.read_with_offset(1, 7, &mut output).unwrap(); + assert_eq!(expected, output); + } + + #[test] + fn fixed_circular_buffer_correctly_reads_buffer_with_repeating_multiple_bytes() { + let circular_buffer = FixedCircularBuffer { + buffer: vec![11, 12, 13, 14, 15, 16, 7, 8, 9, 10], + position: 6, + }; + let expected = vec![14, 15, 16, 14, 15, 16, 14]; + + let mut output = Vec::with_capacity(expected.len()); + circular_buffer.read_with_offset(3, 7, &mut output).unwrap(); + assert_eq!(expected, output); + } + + #[test] + fn fixed_circular_buffer_correctly_reads_buffer_with_repeating_multiple_bytes_from_end_to_start() { + let circular_buffer = FixedCircularBuffer { + buffer: vec![11, 12, 3, 4, 5, 6, 7, 8, 9, 10], + position: 2, + }; + let expected = vec![9, 10, 11, 12, 9, 10, 11]; + + let mut output = Vec::with_capacity(expected.len()); + circular_buffer.read_with_offset(4, 7, &mut output).unwrap(); + assert_eq!(expected, output); + } +} diff --git a/crates/ironrdp-graphics/src/zgfx/control_messages.rs b/crates/ironrdp-graphics/src/zgfx/control_messages.rs new file mode 100644 index 00000000..9e696c03 --- /dev/null +++ b/crates/ironrdp-graphics/src/zgfx/control_messages.rs @@ -0,0 +1,160 @@ +use bit_field::BitField as _; +use bitflags::bitflags; +use byteorder::{LittleEndian, ReadBytesExt as _}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use super::ZgfxError; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SegmentedDataPdu<'a> { + Single(BulkEncodedData<'a>), + Multipart { + uncompressed_size: usize, + segments: Vec>, + }, +} + +impl<'a> SegmentedDataPdu<'a> { + pub(crate) fn from_buffer(mut buffer: &'a [u8]) -> Result { + let descriptor = + SegmentedDescriptor::from_u8(buffer.read_u8()?).ok_or(ZgfxError::InvalidSegmentedDescriptor)?; + + match descriptor { + SegmentedDescriptor::Single => Ok(SegmentedDataPdu::Single(BulkEncodedData::from_buffer(buffer)?)), + SegmentedDescriptor::Multipart => { + let segment_count = usize::from(buffer.read_u16::()?); + let uncompressed_size = usize::try_from(buffer.read_u32::()?) + .map_err(|_| ZgfxError::InvalidIntegralConversion("segments uncompressed size"))?; + + let mut segments = Vec::with_capacity(segment_count); + for _ in 0..segment_count { + let size = usize::try_from(buffer.read_u32::()?) + .map_err(|_| ZgfxError::InvalidIntegralConversion("segment data size"))?; + let (segment_data, new_buffer) = buffer.split_at(size); + buffer = new_buffer; + + segments.push(BulkEncodedData::from_buffer(segment_data)?); + } + + Ok(SegmentedDataPdu::Multipart { + uncompressed_size, + segments, + }) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct BulkEncodedData<'a> { + pub(crate) compression_flags: CompressionFlags, + pub(crate) data: &'a [u8], +} + +impl<'a> BulkEncodedData<'a> { + pub(crate) fn from_buffer(mut buffer: &'a [u8]) -> Result { + let compression_type_and_flags = buffer.read_u8()?; + let _compression_type = CompressionType::from_u8(compression_type_and_flags.get_bits(..4)) + .ok_or(ZgfxError::InvalidCompressionType)?; + let compression_flags = CompressionFlags::from_bits_truncate(compression_type_and_flags.get_bits(4..)); + + Ok(Self { + compression_flags, + data: buffer, + }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive)] +enum SegmentedDescriptor { + Single = 0xe0, + Multipart = 0xe1, +} + +#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive)] +enum CompressionType { + Rdp8 = 0x4, +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CompressionFlags: u8 { + const COMPRESSED = 0x2; + } +} + +#[cfg(test)] +mod test { + use std::sync::LazyLock; + + use super::*; + + const SINGLE_SEGMENTED_DATA_PDU_BUFFER: [u8; 17] = [ + 0xe0, // descriptor + 0x24, // flags and compression type + 0x09, 0xe3, 0x18, 0x0a, 0x44, 0x8d, 0x37, 0xf4, 0xc6, 0xe8, 0xa0, 0x20, 0xc6, 0x30, 0x01, // data + ]; + + const MULTIPART_SEGMENTED_DATA_PDU_BUFFER: [u8; 66] = [ + 0xE1, // descriptor + 0x03, 0x00, // segment count + 0x2B, 0x00, 0x00, 0x00, // uncompressed size + 0x11, 0x00, 0x00, 0x00, // size of the first segment + 0x04, // the first segment: flags and compression type + 0x54, 0x68, 0x65, 0x20, 0x71, 0x75, 0x69, 0x63, 0x6B, 0x20, 0x62, 0x72, 0x6F, 0x77, 0x6E, + 0x20, // the first segment: data + 0x0E, 0x00, 0x00, 0x00, // size of the second segment + 0x04, // the second segment: flags and compression type + 0x66, 0x6F, 0x78, 0x20, 0x6A, 0x75, 0x6D, 0x70, 0x73, 0x20, 0x6F, 0x76, 0x65, // the second segment: data + 0x10, 0x00, 0x00, 0x00, // size of the third segment + 0x24, // the third segment: flags and compression type + 0x39, 0x08, 0x0E, 0x91, 0xF8, 0xD8, 0x61, 0x3D, 0x1E, 0x44, 0x06, 0x43, 0x79, 0x9C, + 0x02, // the third segment: data + ]; + + static SINGLE_SEGMENTED_DATA_PDU: LazyLock> = LazyLock::new(|| { + SegmentedDataPdu::Single(BulkEncodedData { + compression_flags: CompressionFlags::COMPRESSED, + data: &SINGLE_SEGMENTED_DATA_PDU_BUFFER[2..], + }) + }); + static MULTIPART_SEGMENTED_DATA_PDU: LazyLock> = + LazyLock::new(|| SegmentedDataPdu::Multipart { + uncompressed_size: 0x2B, + segments: vec![ + BulkEncodedData { + compression_flags: CompressionFlags::empty(), + data: &MULTIPART_SEGMENTED_DATA_PDU_BUFFER[12..12 + 16], + }, + BulkEncodedData { + compression_flags: CompressionFlags::empty(), + data: &MULTIPART_SEGMENTED_DATA_PDU_BUFFER[33..33 + 13], + }, + BulkEncodedData { + compression_flags: CompressionFlags::COMPRESSED, + data: &MULTIPART_SEGMENTED_DATA_PDU_BUFFER[51..], + }, + ], + }); + + #[test] + fn from_buffer_correctly_parses_zgfx_single_segmented_data_pdu() { + let buffer = SINGLE_SEGMENTED_DATA_PDU_BUFFER.as_ref(); + + assert_eq!( + *SINGLE_SEGMENTED_DATA_PDU, + SegmentedDataPdu::from_buffer(buffer).unwrap() + ); + } + + #[test] + fn from_buffer_correctly_parses_zgfx_multipart_segmented_data_pdu() { + let buffer = MULTIPART_SEGMENTED_DATA_PDU_BUFFER.as_ref(); + + assert_eq!( + *MULTIPART_SEGMENTED_DATA_PDU, + SegmentedDataPdu::from_buffer(buffer).unwrap() + ); + } +} diff --git a/crates/ironrdp-graphics/src/zgfx/mod.rs b/crates/ironrdp-graphics/src/zgfx/mod.rs new file mode 100644 index 00000000..688e655d --- /dev/null +++ b/crates/ironrdp-graphics/src/zgfx/mod.rs @@ -0,0 +1,671 @@ +//! ZGFX (RDP8) Bulk Data Compression + +mod circular_buffer; +mod control_messages; + +use std::io::{self, Write as _}; +use std::sync::LazyLock; + +use bitvec::bits; +use bitvec::field::BitField as _; +use bitvec::order::Msb0; +use bitvec::slice::BitSlice; +use byteorder::WriteBytesExt as _; + +use self::circular_buffer::FixedCircularBuffer; +use self::control_messages::{BulkEncodedData, CompressionFlags, SegmentedDataPdu}; +use crate::utils::Bits; + +const HISTORY_SIZE: usize = 2_500_000; + +pub struct Decompressor { + history: FixedCircularBuffer, +} + +impl Decompressor { + pub fn new() -> Self { + Self { + history: FixedCircularBuffer::new(HISTORY_SIZE), + } + } + + pub fn decompress(&mut self, input: &[u8], output: &mut Vec) -> Result { + let segmented_data = SegmentedDataPdu::from_buffer(input)?; + + match segmented_data { + SegmentedDataPdu::Single(segment) => self.handle_segment(&segment, output), + SegmentedDataPdu::Multipart { + uncompressed_size, + segments, + } => { + let mut bytes_written = 0; + for segment in segments { + let written = self.handle_segment(&segment, output)?; + bytes_written += written; + } + + if bytes_written != uncompressed_size { + Err(ZgfxError::InvalidDecompressedSize { + decompressed_size: bytes_written, + uncompressed_size, + }) + } else { + Ok(bytes_written) + } + } + } + } + + fn handle_segment(&mut self, segment: &BulkEncodedData<'_>, output: &mut Vec) -> Result { + if !segment.data.is_empty() { + if segment.compression_flags.contains(CompressionFlags::COMPRESSED) { + self.decompress_segment(segment.data, output) + } else { + self.history.write_all(segment.data)?; + output.extend_from_slice(segment.data); + + Ok(segment.data.len()) + } + } else { + Ok(0) + } + } + + fn decompress_segment(&mut self, encoded_data: &[u8], output: &mut Vec) -> Result { + if encoded_data.is_empty() { + return Ok(0); + } + + let mut bits = BitSlice::from_slice(encoded_data); + + // The value of the last byte indicates the number of unused bits in the final byte + bits = &bits + [..8 * (encoded_data.len() - 1) - usize::from(*encoded_data.last().expect("encoded_data is not empty"))]; + let mut bits = Bits::new(bits); + let mut bytes_written = 0; + + while !bits.is_empty() { + let token = TOKEN_TABLE + .iter() + .find(|token| token.prefix == bits[..token.prefix.len()]) + .ok_or(ZgfxError::TokenBitsNotFound)?; + let _prefix = bits.split_to(token.prefix.len()); + + match token.ty { + TokenType::NullLiteral => { + // The prefix value is encoded with a "0" prefix, + // then read 8 bits containing the byte to output. + let value = bits.split_to(8).load_be::(); + + self.history.write_u8(value)?; + output.push(value); + bytes_written += 1; + } + TokenType::Literal { literal_value } => { + self.history + .write_u8(literal_value) + .expect("circular buffer does not fail"); + output.push(literal_value); + bytes_written += 1; + } + TokenType::Match { + distance_value_size, + distance_base, + } => { + let written = + handle_match(&mut bits, distance_value_size, distance_base, &mut self.history, output)?; + bytes_written += written; + } + } + } + + Ok(bytes_written) + } +} + +impl Default for Decompressor { + fn default() -> Self { + Self::new() + } +} + +fn handle_match( + bits: &mut Bits<'_>, + distance_value_size: usize, + distance_base: u32, + history: &mut FixedCircularBuffer, + output: &mut Vec, +) -> Result { + // Each token has been assigned a different base distance + // and number of additional value bits to be added to compute the full distance. + + let distance = usize::try_from(distance_base + bits.split_to(distance_value_size).load_be::()) + .map_err(|_| ZgfxError::InvalidIntegralConversion("token's full distance"))?; + + if distance == 0 { + read_unencoded_bytes(bits, history, output).map_err(ZgfxError::from) + } else { + read_encoded_bytes(bits, distance, history, output) + } +} + +fn read_unencoded_bytes( + bits: &mut Bits<'_>, + history: &mut FixedCircularBuffer, + output: &mut Vec, +) -> io::Result { + // A match distance of zero is a special case, + // which indicates that an unencoded run of bytes follows. + // The count of bytes is encoded as a 15-bit value + let length = bits.split_to(15).load_be::(); + + if bits.remaining_bits_of_last_byte() > 0 { + let pad_to_byte_boundary = 8 - bits.remaining_bits_of_last_byte(); + bits.split_to(pad_to_byte_boundary); + } + + let unencoded_bits = bits.split_to(length * 8); + + // FIXME: not very efficient, but we need to rework the `Bits` helper and refactor a bit otherwise + let unencoded_bits = unencoded_bits.to_bitvec(); + let unencoded_bytes = unencoded_bits.as_raw_slice(); + history.write_all(unencoded_bytes)?; + output.extend_from_slice(unencoded_bytes); + + Ok(unencoded_bytes.len()) +} + +fn read_encoded_bytes( + bits: &mut Bits<'_>, + distance: usize, + history: &mut FixedCircularBuffer, + output: &mut Vec, +) -> Result { + // A match length prefix follows the token and indicates + // how many additional bits will be needed to get the full length + // (the number of bytes to be copied). + + let length_token_size = bits.leading_ones(); + bits.split_to(length_token_size + 1); // length token + zero bit + + let length = if length_token_size == 0 { + // special case + + 3 + } else { + let length = bits.split_to(length_token_size + 1).load_be::(); + + let length_token_size = u32::try_from(length_token_size) + .map_err(|_| ZgfxError::InvalidIntegralConversion("length of the token size"))?; + + let base = 2usize.pow(length_token_size + 1); + + base + length + }; + + let output_length = output.len(); + history.read_with_offset(distance, length, output)?; + history + .write_all(&output[output_length..]) + .expect("circular buffer does not fail"); + + Ok(length) +} + +struct Token { + prefix: &'static BitSlice, + ty: TokenType, +} + +enum TokenType { + NullLiteral, + Literal { + literal_value: u8, + }, + Match { + distance_value_size: usize, + distance_base: u32, + }, +} + +static TOKEN_TABLE: LazyLock<[Token; 40]> = LazyLock::new(|| { + [ + Token { + prefix: bits![static u8, Msb0; 0], + ty: TokenType::NullLiteral, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 0, 0, 0], + ty: TokenType::Literal { literal_value: 0x00 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 0, 0, 1], + ty: TokenType::Literal { literal_value: 0x01 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 0, 1, 0, 0], + ty: TokenType::Literal { literal_value: 0x02 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 0, 1, 0, 1], + ty: TokenType::Literal { literal_value: 0x03 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 0, 1, 1, 0], + ty: TokenType::Literal { literal_value: 0x0ff }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 0, 1, 1, 1, 0], + ty: TokenType::Literal { literal_value: 0x04 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 0, 1, 1, 1, 1], + ty: TokenType::Literal { literal_value: 0x05 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 0, 0, 0, 0], + ty: TokenType::Literal { literal_value: 0x06 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 0, 0, 0, 1], + ty: TokenType::Literal { literal_value: 0x07 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 0, 0, 1, 0], + ty: TokenType::Literal { literal_value: 0x08 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 0, 0, 1, 1], + ty: TokenType::Literal { literal_value: 0x09 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 0, 1, 0, 0], + ty: TokenType::Literal { literal_value: 0x0a }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 0, 1, 0, 1], + ty: TokenType::Literal { literal_value: 0x0b }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 0, 1, 1, 0], + ty: TokenType::Literal { literal_value: 0x3a }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 0, 1, 1, 1], + ty: TokenType::Literal { literal_value: 0x3b }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 0, 0, 0], + ty: TokenType::Literal { literal_value: 0x3c }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 0, 0, 1], + ty: TokenType::Literal { literal_value: 0x3d }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 0, 1, 0], + ty: TokenType::Literal { literal_value: 0x3e }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 0, 1, 1], + ty: TokenType::Literal { literal_value: 0x3f }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 1, 0, 0], + ty: TokenType::Literal { literal_value: 0x40 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 1, 0, 1], + ty: TokenType::Literal { literal_value: 0x80 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 1, 1, 0, 0], + ty: TokenType::Literal { literal_value: 0x0c }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 1, 1, 0, 1], + ty: TokenType::Literal { literal_value: 0x38 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 1, 1, 1, 0], + ty: TokenType::Literal { literal_value: 0x39 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 1, 1, 1, 1, 1, 1, 1], + ty: TokenType::Literal { literal_value: 0x66 }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 0, 0, 1], + ty: TokenType::Match { + distance_value_size: 5, + distance_base: 0, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 0, 1, 0], + ty: TokenType::Match { + distance_value_size: 7, + distance_base: 32, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 0, 1, 1], + ty: TokenType::Match { + distance_value_size: 9, + distance_base: 160, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 0, 0], + ty: TokenType::Match { + distance_value_size: 10, + distance_base: 672, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 0, 1], + ty: TokenType::Match { + distance_value_size: 12, + distance_base: 1_696, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 1, 0, 0], + ty: TokenType::Match { + distance_value_size: 14, + distance_base: 5_792, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 1, 0, 1], + ty: TokenType::Match { + distance_value_size: 15, + distance_base: 22_176, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 1, 1, 0, 0], + ty: TokenType::Match { + distance_value_size: 18, + distance_base: 54_944, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 1, 1, 0, 1], + ty: TokenType::Match { + distance_value_size: 20, + distance_base: 317_088, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 1, 1, 1, 0, 0], + ty: TokenType::Match { + distance_value_size: 20, + distance_base: 1_365_664, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 1, 1, 1, 0, 1], + ty: TokenType::Match { + distance_value_size: 21, + distance_base: 2_414_240, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 1, 1, 1, 1, 0, 0], + ty: TokenType::Match { + distance_value_size: 22, + distance_base: 4_511_392, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 1, 1, 1, 1, 0, 1], + ty: TokenType::Match { + distance_value_size: 23, + distance_base: 8_705_696, + }, + }, + Token { + prefix: bits![static u8, Msb0; 1, 0, 1, 1, 1, 1, 1, 1, 0], + ty: TokenType::Match { + distance_value_size: 24, + distance_base: 17_094_304, + }, + }, + ] +}); + +#[derive(Debug)] +pub enum ZgfxError { + IOError(io::Error), + InvalidCompressionType, + InvalidSegmentedDescriptor, + InvalidDecompressedSize { + decompressed_size: usize, + uncompressed_size: usize, + }, + TokenBitsNotFound, + InvalidIntegralConversion(&'static str), +} + +impl core::fmt::Display for ZgfxError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::IOError(_error) => write!(f, "IO error"), + Self::InvalidCompressionType => write!(f, "invalid compression type"), + Self::InvalidSegmentedDescriptor => write!(f, "invalid segmented descriptor"), + Self::InvalidDecompressedSize { + decompressed_size, + uncompressed_size, + } => write!( + f, + "decompressed size of segments ({decompressed_size}) does not equal to uncompressed size ({uncompressed_size})", + ), + Self::TokenBitsNotFound => write!(f, "token bits not found"), + Self::InvalidIntegralConversion(type_name) => write!(f, "invalid `{type_name}`: out of range integral type conversion"), + } + } +} + +impl core::error::Error for ZgfxError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::IOError(error) => Some(error), + Self::InvalidCompressionType => None, + Self::InvalidSegmentedDescriptor => None, + Self::InvalidDecompressedSize { .. } => None, + Self::TokenBitsNotFound => None, + Self::InvalidIntegralConversion(_) => None, + } + } +} + +impl From for ZgfxError { + fn from(err: io::Error) -> Self { + Self::IOError(err) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ENCODED_ZGFX_SINGLE: [&[u8]; 5] = [ + include_bytes!("test_assets/encoded.0.bin"), + include_bytes!("test_assets/encoded.1.bin"), + include_bytes!("test_assets/encoded.2.bin"), + include_bytes!("test_assets/encoded.3.bin"), + include_bytes!("test_assets/encoded.4.bin"), + ]; + + const DECODED_ZGFX_SINGLE: [&[u8]; 5] = [ + include_bytes!("test_assets/decoded.0.bin"), + include_bytes!("test_assets/decoded.1.bin"), + include_bytes!("test_assets/decoded.2.bin"), + include_bytes!("test_assets/decoded.3.bin"), + include_bytes!("test_assets/decoded.4.bin"), + ]; + + #[test] + fn zgfx_decompresses_multiple_single_pdus() { + let pairs = ENCODED_ZGFX_SINGLE + .iter() + .copied() + .zip(DECODED_ZGFX_SINGLE.iter().copied()); + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(pairs.clone().map(|(_, d)| d.len()).max().unwrap()); + for (i, (encode, decode)) in pairs.enumerate() { + let bytes_written = zgfx.decompress(encode.as_ref(), &mut decompressed).unwrap(); + assert_eq!(decode.len(), bytes_written); + assert_eq!(decompressed, *decode, "Failed to decompress encoded PDU #{i}"); + decompressed.clear(); + } + } + + #[test] + fn zgfx_decompresses_only_one_literal() { + let buffer = [0b1100_1000, 0x03]; + let expected = vec![0x01]; + + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(expected.len()); + zgfx.decompress_segment(buffer.as_ref(), &mut decompressed).unwrap(); + assert_eq!(decompressed, expected); + } + + #[test] + fn zgfx_decompresses_one_literal_with_null_prefix() { + let buffer = [0b0011_0010, 0b1000_0000, 0x07]; + let expected = vec![0x65]; + + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(expected.len()); + zgfx.decompress_segment(buffer.as_ref(), &mut decompressed).unwrap(); + assert_eq!(decompressed, expected); + } + + #[test] + fn zgfx_decompresses_only_multiple_literals() { + let buffer = [0b1100_1110, 0b1001_1011, 0b0001_1001, 0b0100_0000, 0x06]; + let expected = vec![0x01, 0x02, 0xff, 0x65]; + + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(expected.len()); + zgfx.decompress_segment(buffer.as_ref(), &mut decompressed).unwrap(); + assert_eq!(decompressed, expected); + } + + #[test] + fn zgfx_decompresses_one_literal_with_one_match_distance_1() { + let buffer = [0b0011_0010, 0b1100_0100, 0b0011_0000, 0x1]; + let expected = vec![0x65; 1 + 4]; // literal (1) + match repeated 4 (length) + 0 times + + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(expected.len()); + zgfx.decompress_segment(buffer.as_ref(), &mut decompressed).unwrap(); + assert_eq!(decompressed, expected); + } + + #[test] + fn zgfx_decompresses_three_literals_with_one_match_distance_3_length_57() { + let buffer = [ + 0b0010_0000, + 0b1001_0000, + 0b1000_1000, + 0b0111_0001, + 0b0001_1111, + 0b1011_0010, + 0x1, + ]; + let expected = "ABC".repeat(20); + let expected = expected.as_bytes(); + + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(expected.len()); + zgfx.decompress_segment(buffer.as_ref(), &mut decompressed).unwrap(); + assert_eq!(decompressed, expected); + } + + #[test] + fn zgfx_decompresses_one_match_with_match_unencoded_bytes() { + let expected = "The quick brown fox jumps over the lazy dog".as_bytes(); + let mut buffer = vec![0b1000_1000, 0b0000_0000, 0b00010101, 0b1000_0000]; + buffer.extend_from_slice(expected); + buffer.extend_from_slice(&[0x00]); // no bits unused + + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(expected.len()); + zgfx.decompress_segment(buffer.as_ref(), &mut decompressed).unwrap(); + assert_eq!(decompressed, expected); + } + + #[test] + fn zgfx_decompresses_multiple_literals_with_match_in_center_with_not_compressed() { + let buffer = [ + 0xE1, // DEBLOCK_MULTIPART + 0x03, 0x00, // 3 segments + 0x2B, 0x00, 0x00, 0x00, // 0x0000002B total bytes uncompressed + 0x11, 0x00, 0x00, 0x00, // first segment is the next 17 bytes: + 0x04, // type 4, not PACKET_COMPRESSED + 0x54, 0x68, 0x65, 0x20, 0x71, 0x75, 0x69, 0x63, 0x6B, 0x20, 0x62, 0x72, 0x6F, 0x77, 0x6E, + 0x20, // "The quick brown " + 0x0E, 0x00, 0x00, 0x00, // second segment is the next 14 bytes: + 0x04, // type 4, not PACKET_COMPRESSED + 0x66, 0x6F, 0x78, 0x20, 0x6A, 0x75, 0x6D, 0x70, 0x73, 0x20, 0x6F, 0x76, 0x65, // "fox jumps ove" + 0x10, 0x00, 0x00, 0x00, // third segment is the next 16 bytes + 0x24, // type 4 + PACKET_COMPRESSED + 0x39, 0x08, 0x0E, 0x91, 0xF8, 0xD8, 0x61, 0x3D, 0x1E, 0x44, 0x06, 0x43, 0x79, 0x9C, // encoded: + // 0 01110010 = literal 0x72 = "r" + // 0 00100000 = literal 0x20 = " " + // 0 01110100 = literal 0x74 = "t" + // + // 10001 11111 0 = match, distance = 31, length = 3 "he " + // + // 0 01101100 = literal 0x6C = "l" + // 0 01100001 = literal 0x61 = "a" + // 0 01111010 = literal 0x7A = "z" + // 0 01111001 = literal 0x79 = "y" + // 0 00100000 = literal 0x20 = " " + // 0 01100100 = literal 0x64 = "d" + // 0 01101111 = literal 0x6F = "o" + // 0 01100111 = literal 0x67 = "g" + 0x02, // ignore last two bits of 0x9C byte + ]; + let expected = "The quick brown fox jumps over the lazy dog".as_bytes(); + + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(expected.len()); + let bytes_written = zgfx.decompress(buffer.as_ref(), &mut decompressed).unwrap(); + assert_eq!(expected.len(), bytes_written); + assert_eq!(decompressed, expected, "\n{decompressed:x?} != \n{expected:x?}"); + } + + #[test] + fn zgfx_decompresses_single_match_unencoded_block() { + let buffer = [ + 0xe0, 0x04, 0x13, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x06, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, + ]; + let expected = vec![ + 0x13, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x06, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, + ]; + + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(expected.len()); + let bytes_written = zgfx.decompress(buffer.as_ref(), &mut decompressed).unwrap(); + assert_eq!(expected.len(), bytes_written); + assert_eq!(decompressed, expected); + } + + #[test] + fn zgfx_decompresses_unencoded_block_without_padding() { + let buffer = [0b1110_0101, 0b0001_0000, 0b0000_0000, 0b00000001, 0b1111_0000, 0x0]; + let expected = vec![0x08, 0xf0]; + + let mut zgfx = Decompressor::new(); + let mut decompressed = Vec::with_capacity(expected.len()); + zgfx.decompress_segment(buffer.as_ref(), &mut decompressed).unwrap(); + assert_eq!(decompressed, expected); + } +} diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.0.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.0.bin new file mode 100644 index 00000000..2315768b Binary files /dev/null and b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.0.bin differ diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.1.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.1.bin new file mode 100644 index 00000000..347ac09b Binary files /dev/null and b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.1.bin differ diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.2.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.2.bin new file mode 100644 index 00000000..eadceb09 Binary files /dev/null and b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.2.bin differ diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.3.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.3.bin new file mode 100644 index 00000000..c9148902 Binary files /dev/null and b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.3.bin differ diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.4.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.4.bin new file mode 100644 index 00000000..6ca2be71 Binary files /dev/null and b/crates/ironrdp-graphics/src/zgfx/test_assets/decoded.4.bin differ diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.0.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.0.bin new file mode 100644 index 00000000..e2c67cd9 --- /dev/null +++ b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.0.bin @@ -0,0 +1,2 @@ +$  +D7 0 \ No newline at end of file diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.1.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.1.bin new file mode 100644 index 00000000..dfebf07c --- /dev/null +++ b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.1.bin @@ -0,0 +1 @@ +$EbTc\EȎvimE]f"DGXGR", \ No newline at end of file diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.2.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.2.bin new file mode 100644 index 00000000..000afabf Binary files /dev/null and b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.2.bin differ diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.3.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.3.bin new file mode 100644 index 00000000..bf23dbd1 Binary files /dev/null and b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.3.bin differ diff --git a/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.4.bin b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.4.bin new file mode 100644 index 00000000..4993f654 Binary files /dev/null and b/crates/ironrdp-graphics/src/zgfx/test_assets/encoded.4.bin differ diff --git a/crates/ironrdp-input/CHANGELOG.md b/crates/ironrdp-input/CHANGELOG.md new file mode 100644 index 00000000..21122b4a --- /dev/null +++ b/crates/ironrdp-input/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-input-v0.1.3...ironrdp-input-v0.2.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + + + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-input-v0.1.2...ironrdp-input-v0.1.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-input-v0.1.1...ironrdp-input-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-input-v0.1.0...ironrdp-input-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-input/Cargo.toml b/crates/ironrdp-input/Cargo.toml new file mode 100644 index 00000000..2f3b78c9 --- /dev/null +++ b/crates/ironrdp-input/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ironrdp-input" +version = "0.4.0" +readme = "README.md" +description = "Utilities to manage and build RDP input packets" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public +bitvec = "1.0" +smallvec = "1.15" + +[lints] +workspace = true + diff --git a/crates/ironrdp-input/LICENSE-APACHE b/crates/ironrdp-input/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-input/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-input/LICENSE-MIT b/crates/ironrdp-input/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-input/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-input/README.md b/crates/ironrdp-input/README.md new file mode 100644 index 00000000..cae918d2 --- /dev/null +++ b/crates/ironrdp-input/README.md @@ -0,0 +1,7 @@ +# IronRDP Input + +Helpers to build RDP FastPathInput packets. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-input/src/lib.rs b/crates/ironrdp-input/src/lib.rs new file mode 100644 index 00000000..42292a38 --- /dev/null +++ b/crates/ironrdp-input/src/lib.rs @@ -0,0 +1,452 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +use std::collections::BTreeSet; + +use bitvec::array::BitArray; +use bitvec::BitArr; +use ironrdp_pdu::input::fast_path::{FastPathInputEvent, KeyboardFlags}; +use ironrdp_pdu::input::mouse::PointerFlags; +use ironrdp_pdu::input::mouse_x::PointerXFlags; +use ironrdp_pdu::input::{MousePdu, MouseXPdu}; +use smallvec::SmallVec; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum MouseButton { + Left = 0, + Middle = 1, + Right = 2, + /// Typically Browser Back button + X1 = 3, + /// Typically Browser Forward button + X2 = 4, +} + +impl MouseButton { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_idx(self) -> usize { + self as usize + } + + pub fn from_idx(idx: usize) -> Option { + match idx { + 0 => Some(Self::Left), + 1 => Some(Self::Middle), + 2 => Some(Self::Right), + 3 => Some(Self::X1), + 4 => Some(Self::X2), + _ => None, + } + } + + pub fn from_web_button(value: u8) -> Option { + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button#value + match value { + 0 => Some(Self::Left), + 1 => Some(Self::Middle), + 2 => Some(Self::Right), + 3 => Some(Self::X1), + 4 => Some(Self::X2), + _ => None, + } + } + + pub fn from_native_button(value: u16) -> Option { + match value { + 1 => Some(Self::Left), + 2 => Some(Self::Middle), + 3 => Some(Self::Right), + 8 => Some(Self::X1), + 9 => Some(Self::X2), + _ => None, + } + } +} + +/// Keyboard scan code. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Scancode { + code: u8, + extended: bool, +} + +impl Scancode { + pub const fn from_u8(extended: bool, code: u8) -> Self { + Self { code, extended } + } + + pub const fn from_u16(scancode: u16) -> Self { + let extended = scancode & 0xE000 == 0xE000; + + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "truncating on purpose" + )] + let code = scancode as u8; + + Self { code, extended } + } + + pub fn as_idx(self) -> usize { + if self.extended { + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (integer upcast)")] + usize::from(self.code).checked_add(256).expect("never overflow") + } else { + usize::from(self.code) + } + } + + pub fn as_u8(self) -> (bool, u8) { + (self.extended, self.code) + } + + pub fn as_u16(self) -> u16 { + if self.extended { + u16::from(self.code) | 0xE000 + } else { + u16::from(self.code) + } + } +} + +impl From<(bool, u8)> for Scancode { + fn from((extended, code): (bool, u8)) -> Self { + Self::from_u8(extended, code) + } +} + +impl From for Scancode { + fn from(code: u16) -> Self { + Self::from_u16(code) + } +} + +/// Cursor position for a mouse device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MousePosition { + pub x: u16, + pub y: u16, +} + +/// Mouse wheel rotations. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WheelRotations { + pub is_vertical: bool, + pub rotation_units: i16, +} + +#[derive(Debug, Clone)] +pub enum Operation { + MouseButtonPressed(MouseButton), + MouseButtonReleased(MouseButton), + MouseMove(MousePosition), + WheelRotations(WheelRotations), + KeyPressed(Scancode), + KeyReleased(Scancode), + UnicodeKeyPressed(char), + UnicodeKeyReleased(char), +} + +pub type KeyboardState = BitArr!(for 512); +pub type MouseButtonsState = BitArr!(for 5); + +/// In-memory database for maintaining the current keyboard and mouse state. +pub struct Database { + unicode_keyboard_state: BTreeSet, + keyboard: KeyboardState, + mouse_buttons: MouseButtonsState, + mouse_position: MousePosition, +} + +impl Default for Database { + fn default() -> Self { + Self::new() + } +} + +impl Database { + pub fn new() -> Self { + Self { + keyboard: BitArray::ZERO, + mouse_buttons: BitArray::ZERO, + mouse_position: MousePosition { x: 0, y: 0 }, + unicode_keyboard_state: BTreeSet::new(), + } + } + + pub fn is_unicode_key_pressed(&self, character: char) -> bool { + self.unicode_keyboard_state.contains(&character) + } + + pub fn is_key_pressed(&self, scancode: Scancode) -> bool { + self.keyboard + .get(scancode.as_idx()) + .as_deref() + .copied() + .unwrap_or(false) + } + + pub fn is_mouse_button_pressed(&self, button: MouseButton) -> bool { + self.mouse_buttons + .get(button.as_idx()) + .as_deref() + .copied() + .unwrap_or(false) + } + + pub fn mouse_position(&self) -> MousePosition { + self.mouse_position + } + + pub fn keyboard_state(&self) -> &KeyboardState { + &self.keyboard + } + + pub fn mouse_buttons_state(&self) -> &MouseButtonsState { + &self.mouse_buttons + } + + /// Apply a transaction (list of operations) and returns a list of RDP input events to send. + /// + /// Operations that would cause no state change are ignored. + pub fn apply(&mut self, transaction: impl IntoIterator) -> SmallVec<[FastPathInputEvent; 2]> { + let mut events = SmallVec::new(); + + for operation in transaction { + match operation { + Operation::MouseButtonPressed(button) => { + let was_pressed = self.mouse_buttons.replace(button.as_idx(), true); + + if !was_pressed { + let event = match MouseButtonFlags::from(button) { + MouseButtonFlags::Button(flags) => FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::DOWN | flags, + number_of_wheel_rotation_units: 0, + x_position: self.mouse_position.x, + y_position: self.mouse_position.y, + }), + MouseButtonFlags::Pointer(flags) => FastPathInputEvent::MouseEventEx(MouseXPdu { + flags: PointerXFlags::DOWN | flags, + x_position: self.mouse_position.x, + y_position: self.mouse_position.y, + }), + }; + + events.push(event) + } + } + Operation::MouseButtonReleased(button) => { + let was_pressed = self.mouse_buttons.replace(button.as_idx(), false); + + if was_pressed { + let event = match MouseButtonFlags::from(button) { + MouseButtonFlags::Button(flags) => FastPathInputEvent::MouseEvent(MousePdu { + flags, + number_of_wheel_rotation_units: 0, + x_position: self.mouse_position.x, + y_position: self.mouse_position.y, + }), + MouseButtonFlags::Pointer(flags) => FastPathInputEvent::MouseEventEx(MouseXPdu { + flags, + x_position: self.mouse_position.x, + y_position: self.mouse_position.y, + }), + }; + + events.push(event) + } + } + Operation::MouseMove(position) => { + if position != self.mouse_position { + self.mouse_position = position; + events.push(FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::MOVE, + number_of_wheel_rotation_units: 0, + x_position: position.x, + y_position: position.y, + })) + } + } + Operation::WheelRotations(rotations) => events.push(FastPathInputEvent::MouseEvent(MousePdu { + flags: if rotations.is_vertical { + PointerFlags::VERTICAL_WHEEL + } else { + PointerFlags::HORIZONTAL_WHEEL + }, + number_of_wheel_rotation_units: rotations.rotation_units, + x_position: self.mouse_position.x, + y_position: self.mouse_position.y, + })), + Operation::KeyPressed(scancode) => { + let was_pressed = self.keyboard.replace(scancode.as_idx(), true); + + let mut flags = KeyboardFlags::empty(); + + if scancode.extended { + flags |= KeyboardFlags::EXTENDED + }; + + if was_pressed { + events.push(FastPathInputEvent::KeyboardEvent( + flags | KeyboardFlags::RELEASE, + scancode.code, + )); + } + + events.push(FastPathInputEvent::KeyboardEvent(flags, scancode.code)); + } + Operation::KeyReleased(scancode) => { + let was_pressed = self.keyboard.replace(scancode.as_idx(), false); + + let mut flags = KeyboardFlags::RELEASE; + + if scancode.extended { + flags |= KeyboardFlags::EXTENDED + }; + + if was_pressed { + events.push(FastPathInputEvent::KeyboardEvent(flags, scancode.code)); + } + } + Operation::UnicodeKeyPressed(character) => { + let was_pressed = !self.unicode_keyboard_state.insert(character); + + let mut utf16_buffer = [0u16; 2]; + let utf16_code_units = character.encode_utf16(&mut utf16_buffer); + + if was_pressed { + for code in utf16_code_units.iter() { + events.push(FastPathInputEvent::UnicodeKeyboardEvent(KeyboardFlags::RELEASE, *code)); + } + } + + for code in utf16_code_units { + events.push(FastPathInputEvent::UnicodeKeyboardEvent(KeyboardFlags::empty(), *code)); + } + } + Operation::UnicodeKeyReleased(character) => { + let was_pressed = self.unicode_keyboard_state.remove(&character); + + let mut utf16_buffer = [0u16; 2]; + let utf16_code_units = character.encode_utf16(&mut utf16_buffer); + + if was_pressed { + for code in utf16_code_units { + events.push(FastPathInputEvent::UnicodeKeyboardEvent(KeyboardFlags::RELEASE, *code)); + } + } + } + } + } + + events + } + + /// Releases all keys and buttons. Returns a list of RDP input events to send. + pub fn release_all(&mut self) -> SmallVec<[FastPathInputEvent; 2]> { + let mut events = SmallVec::new(); + + for idx in self.mouse_buttons.iter_ones() { + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")] + let button = MouseButton::from_idx(idx).expect("in-range index"); + + let event = match MouseButtonFlags::from(button) { + MouseButtonFlags::Button(flags) => FastPathInputEvent::MouseEvent(MousePdu { + flags, + number_of_wheel_rotation_units: 0, + x_position: self.mouse_position.x, + y_position: self.mouse_position.y, + }), + MouseButtonFlags::Pointer(flags) => FastPathInputEvent::MouseEventEx(MouseXPdu { + flags, + x_position: self.mouse_position.x, + y_position: self.mouse_position.y, + }), + }; + + events.push(event) + } + + // The keyboard bit array size is 512. + for idx in self.keyboard.iter_ones() { + let (scancode, extended) = if idx >= 256 { + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer underflow)")] + let extended_code = idx.checked_sub(256).expect("never underflow"); + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")] + (u8::try_from(extended_code).expect("always in the range"), true) + } else { + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")] + (u8::try_from(idx).expect("always in the range"), false) + }; + + let mut flags = KeyboardFlags::RELEASE; + + if extended { + flags |= KeyboardFlags::EXTENDED + }; + + events.push(FastPathInputEvent::KeyboardEvent(flags, scancode)); + } + + for character in core::mem::take(&mut self.unicode_keyboard_state).into_iter() { + let mut utf16_buffer = [0u16; 2]; + let utf16_code_units = character.encode_utf16(&mut utf16_buffer); + + for code in utf16_code_units { + events.push(FastPathInputEvent::UnicodeKeyboardEvent(KeyboardFlags::RELEASE, *code)); + } + } + + self.mouse_buttons = BitArray::ZERO; + self.keyboard = BitArray::ZERO; + + events + } +} + +/// Returns the RDP input event to send in order to synchronize lock keys. +pub fn synchronize_event(scroll_lock: bool, num_lock: bool, caps_lock: bool, kana_lock: bool) -> FastPathInputEvent { + use ironrdp_pdu::input::fast_path::SynchronizeFlags; + + let mut flags = SynchronizeFlags::empty(); + + if scroll_lock { + flags |= SynchronizeFlags::SCROLL_LOCK; + } + + if num_lock { + flags |= SynchronizeFlags::NUM_LOCK; + } + + if caps_lock { + flags |= SynchronizeFlags::CAPS_LOCK; + } + + if kana_lock { + flags |= SynchronizeFlags::KANA_LOCK; + } + + FastPathInputEvent::SyncEvent(flags) +} + +enum MouseButtonFlags { + Button(PointerFlags), + Pointer(PointerXFlags), +} + +impl From for MouseButtonFlags { + fn from(value: MouseButton) -> Self { + match value { + MouseButton::Left => Self::Button(PointerFlags::LEFT_BUTTON), + MouseButton::Middle => Self::Button(PointerFlags::MIDDLE_BUTTON_OR_WHEEL), + MouseButton::Right => Self::Button(PointerFlags::RIGHT_BUTTON), + MouseButton::X1 => Self::Pointer(PointerXFlags::BUTTON1), + MouseButton::X2 => Self::Pointer(PointerXFlags::BUTTON2), + } + } +} diff --git a/crates/ironrdp-mstsgu/Cargo.toml b/crates/ironrdp-mstsgu/Cargo.toml new file mode 100644 index 00000000..b194f467 --- /dev/null +++ b/crates/ironrdp-mstsgu/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "ironrdp-mstsgu" +version = "0.0.1" +readme = "README.md" +description = "Terminal Services Gateway Server Protocol" +publish = false # TODO: publish +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"] +native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"] + +[dependencies] +base64 = "0.22" +bitflags = "2.9" +futures-util = "0.3" +http-body-util = { version = "0.1" } +hyper-util = { version = "0.1", features = ["tokio"] } +hyper = { version = "1.7", features = ["client", "http1"] } +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["std"] } +ironrdp-error = { path = "../ironrdp-error", version = "0.1" } +ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" } +log = "0.4" +tokio-tungstenite = { version = "0.28" } +tokio-util = { version = "0.7" } +tokio = { version = "1.43", features = ["macros", "rt"] } +uuid = { version = "1.19", features = ["v4"] } + +[lints] +workspace = true diff --git a/crates/ironrdp-mstsgu/README.md b/crates/ironrdp-mstsgu/README.md new file mode 100644 index 00000000..8b1d171c --- /dev/null +++ b/crates/ironrdp-mstsgu/README.md @@ -0,0 +1,11 @@ +# IronRDP MS-TSGU + +[Terminal Services Gateway Server Protocol][MS-TSGU] implementation for IronRDP. + +This crate +- implements an MVP state needed to connect through Microsoft RD Gateway, +- only supports the HTTPS protocol with WebSocket (and not the legacy HTTP, HTTP-RPC or UDP protocols), +- does not implement reconnection/reauthentication, and +- only supports basic auth. + +[MS-TSGU]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/0007d661-a86d-4e8f-89f7-7f77f8824188 diff --git a/crates/ironrdp-mstsgu/src/lib.rs b/crates/ironrdp-mstsgu/src/lib.rs new file mode 100644 index 00000000..b8b27c84 --- /dev/null +++ b/crates/ironrdp-mstsgu/src/lib.rs @@ -0,0 +1,464 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +#[macro_use] +mod macros; + +mod proto; + +use core::fmt; +use core::fmt::Display; +use core::pin::Pin; +use core::task::Poll; +use core::time::Duration; +use std::io; + +use base64::engine::general_purpose::STANDARD; +use base64::Engine as _; +use futures_util::stream::{SplitSink, SplitStream}; +use futures_util::{FutureExt as _, SinkExt as _, StreamExt as _}; +use hyper::body::Bytes; +use ironrdp_core::{Decode as _, Encode, ReadCursor, WriteCursor}; +use ironrdp_tls::TlsStream; +use log::{error, warn}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpStream; +use tokio::sync::oneshot; +use tokio_tungstenite::tungstenite::handshake::client::generate_key; +use tokio_tungstenite::tungstenite::protocol::Role; +use tokio_tungstenite::tungstenite::{http, Message}; +use tokio_tungstenite::WebSocketStream; +use tokio_util::sync::PollSender; + +use self::proto::{ + ChannelPkt, ChannelResp, DataPkt, HandshakeReqPkt, HandshakeRespPkt, HttpCapsTy, KeepalivePkt, PktHdr, PktTy, + TunnelAuthPkt, TunnelAuthRespPkt, TunnelReqPkt, TunnelRespPkt, +}; + +#[derive(Clone, Debug)] +pub struct GwConnectTarget { + pub gw_endpoint: String, + pub gw_user: String, + pub gw_pass: String, + + pub server: String, +} + +type Error = ironrdp_error::Error; + +#[derive(Debug)] +#[non_exhaustive] +pub enum GwErrorKind { + InvalidGwTarget, + Connect, + PacketEof, + UnsupportedFeature, + Custom, + Encode, + Decode, +} + +trait GwErrorExt { + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static; +} + +impl GwErrorExt for ironrdp_error::Error { + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static, + { + Self::new(context, GwErrorKind::Custom).with_source(e) + } +} + +impl Display for GwErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let x = match self { + GwErrorKind::InvalidGwTarget => "invalid GW Target", + GwErrorKind::Connect => "connection error", + GwErrorKind::PacketEof => "PacketEOF", + GwErrorKind::UnsupportedFeature => "unsupported feature", + GwErrorKind::Custom => "custom", + GwErrorKind::Encode => "encode", + GwErrorKind::Decode => "decode", + }; + f.write_str(x) + } +} + +impl core::error::Error for GwErrorKind {} + +struct GwConn { + client_name: String, + target: GwConnectTarget, + ws_sink: SplitSink>, Message>, + ws_stream: SplitStream>>, +} + +pub struct GwClient { + work: tokio::task::JoinHandle>, + rx: tokio::sync::mpsc::Receiver, + rx_bufs: Vec, + tx: PollSender, +} + +impl Drop for GwClient { + fn drop(&mut self) { + self.work.abort(); + } +} + +impl GwClient { + pub async fn connect( + target: &GwConnectTarget, + client_name: &str, + ) -> Result<(GwClient, core::net::SocketAddr), Error> { + let gw_host = target + .gw_endpoint + .split(":") + .nth(0) + .ok_or_else(|| Error::new("Connect", GwErrorKind::InvalidGwTarget))?; + + let stream = TcpStream::connect(&target.gw_endpoint) + .await + .map_err(|e| custom_err!("TCP connect", e))?; + let client_addr = stream + .local_addr() + .map_err(|e| custom_err!("get socket local address", e))?; + + let (stream, _) = ironrdp_tls::upgrade(stream, gw_host) + .await + .map_err(|e| custom_err!("TLS connect", e))?; + + let auth_val: String = STANDARD.encode(format!("{}:{}", target.gw_user, target.gw_pass)); + let req = http::Request::builder() + .method("RDG_OUT_DATA") + .header(hyper::header::HOST, gw_host) + .header("Rdg-Connection-Id", format!("{{{}}}", uuid::Uuid::new_v4())) + .uri("/remoteDesktopGateway/") + .header(hyper::header::AUTHORIZATION, format!("Basic {auth_val}")) + .header(hyper::header::CONNECTION, "Upgrade") + .header(hyper::header::UPGRADE, "websocket") + .header(hyper::header::SEC_WEBSOCKET_VERSION, "13") + .header(hyper::header::SEC_WEBSOCKET_KEY, generate_key()) + .body(http_body_util::Empty::::new()) + .map_err(|e| custom_err!("failed to build request", e))?; + + let stream = hyper_util::rt::tokio::TokioIo::new(stream); + let (mut sender, mut conn) = hyper::client::conn::http1::handshake(stream) + .await + .map_err(|e| custom_err!("H1 Handshake", e))?; + let (tx, rx) = oneshot::channel(); + + let jh = tokio::task::spawn(async move { + tokio::select! { + Err(e) = &mut conn => error!("Handshake error: {:?}", e), + _ = rx => (), + } + conn.into_parts() + }); + let resp = sender + .send_request(req) + .await + .map_err(|e| custom_err!("WS Upgrade Send error", e))?; + + if resp.status() != http::StatusCode::SWITCHING_PROTOCOLS { + return Err(Error::new("WS Upgrade", GwErrorKind::Connect)); + } + + let _ = tx.send(()); // TODO: Not needed since it doesnt keep alive conn? + let stream = jh.await.map_err(|e| custom_err!("WS join", e))?.io.into_inner(); + + Self::connect_ws(target.clone(), client_name, stream) + .await + .map(|x| (x, client_addr)) + } + + async fn connect_ws( + target: GwConnectTarget, + client_name: &str, + tls_stream: TlsStream, + ) -> Result { + let ws_stream: WebSocketStream<_> = WebSocketStream::from_raw_socket(tls_stream, Role::Client, None).await; + let (ws_sink, ws_stream) = ws_stream.split(); + let mut gw = GwConn { + client_name: client_name.to_owned(), + target, + ws_sink, + ws_stream, + }; + + gw.handshake().await?; + gw.tunnel().await?; + gw.tunnel_auth().await?; + gw.channel().await?; + + let (in_tx, in_rx) = tokio::sync::mpsc::channel(4); + let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::(4); + + let work = tokio::spawn(async move { + let iv = Duration::from_secs(15 * 60); + let mut keepalive_interval = tokio::time::interval_at(tokio::time::Instant::now() + iv, iv); + + loop { + let mut wsbuf = [0u8; 8192]; + + tokio::select!( + _ = keepalive_interval.tick() => { + let pos = { + let mut cur = WriteCursor::new(&mut wsbuf); + KeepalivePkt.encode(&mut cur).map_err(|e| custom_err!("PktEncode", e))?; + cur.pos() + }; + + gw.ws_sink.send(Message::Binary(Bytes::copy_from_slice(&wsbuf[..pos]))).await.map_err(|e| custom_err!("ws send", e))?; + }, + next = gw.ws_stream.next() => { + let tmp = next.ok_or_else(|| Error::new("WS Stream Dead", GwErrorKind::Connect))?; + let msg = tmp.map_err(|e| custom_err!("Stream", e))?.into_data(); + let mut cur = ReadCursor::new(&msg); + let hdr = PktHdr::decode(&mut cur).map_err(|e| custom_err!("Header Decode", e))?; + + let header_length = usize::try_from(hdr.length).map_err(|_| Error::new("PktHdr too big", GwErrorKind::Decode))?; + assert!(cur.len() >= header_length - hdr.size()); + match hdr.ty { + PktTy::Keepalive => { + continue; + }, + PktTy::Data => { + let p = DataPkt::decode(&mut cur).map_err(|e| custom_err!("PktDecode", e))?; + in_tx.send(Bytes::from(p.data.to_vec())).await.map_err(|e| custom_err!("in_tx dead", e))?; + }, + x => { + warn!("Unhandled gw packet type {x:?}"); + } + } + }, + next = out_rx.recv() => { + let next = next.ok_or_else(|| Error::new("WS Sink Dead", GwErrorKind::Connect))?; + let pkt = DataPkt { data: &next }; + + let pos = { + let mut cur = WriteCursor::new(&mut wsbuf); + pkt.encode(&mut cur).map_err(|e| custom_err!("PktEncode", e))?; + cur.pos() + }; + gw.ws_sink.send(Message::Binary(Bytes::copy_from_slice(&wsbuf[..pos]))).await.map_err(|e| custom_err!("ws send", e))?; + } + ); + } + }); + + Ok(GwClient { + work, + rx: in_rx, + rx_bufs: vec![], + tx: PollSender::new(out_tx), + }) + } +} + +impl GwConn { + async fn send_packet(&mut self, payload: &E) -> Result<(), Error> { + let mut buf = [0u8; 4096]; + let pos = { + let mut cur = WriteCursor::new(&mut buf); + payload + .encode(&mut cur) + .map_err(|e| Error::new("packet encode", GwErrorKind::Encode).with_source(e))?; + cur.pos() + }; + self.ws_sink + .send(Message::Binary(Bytes::copy_from_slice(&buf[..pos]))) + .await + .map_err(|e| custom_err!("WebSocket send error", e))?; + Ok(()) + } + + async fn read_packet(&mut self) -> Result<(PktHdr, Bytes), Error> { + let mut msg = self + .ws_stream + .next() + .await + .ok_or_else(|| Error::new("Stream closed", GwErrorKind::Connect))? + .map_err(|e| custom_err!("WS err", e))? + .into_data(); + let mut cur = ReadCursor::new(&msg); + + let hdr = PktHdr::decode(&mut cur).map_err(|_| Error::new("PktHdr", GwErrorKind::Decode))?; + + let header_length = + usize::try_from(hdr.length).map_err(|_| Error::new("PktHdr too big", GwErrorKind::Decode))?; + if cur.len() != header_length - hdr.size() { + return Err(Error::new("read_packet", GwErrorKind::PacketEof)); + } + + Ok((hdr, msg.split_off(cur.pos()))) + } + + async fn handshake(&mut self) -> Result<(), Error> { + // For NTLM we would include extended_auth: NTLM_SSPI in this handshake req here. + let hs = HandshakeReqPkt { + ver_major: 1, + ver_minor: 0, + ..HandshakeReqPkt::default() + }; + self.send_packet(&hs).await?; + let (_hdr, bytes) = self.read_packet().await?; + + let mut cur = ReadCursor::new(&bytes); + let resp = HandshakeRespPkt::decode(&mut cur).map_err(|_| Error::new("Handshake", GwErrorKind::Decode))?; + if resp.error_code != 0 || resp.ver_major != 1 || resp.ver_minor != 0 || resp.server_version != 0 { + return Err(Error::new("Handshake", GwErrorKind::Connect)); + } + Ok(()) + } + + async fn tunnel(&mut self) -> Result<(), Error> { + let req = TunnelReqPkt { + // Havent seen any server working without this. + caps: HttpCapsTy::MessagingConsentSign.as_u32(), + fields_present: 0, + ..TunnelReqPkt::default() + }; + self.send_packet(&req).await?; + + let (_hdr, bytes) = self.read_packet().await?; + let mut cur = ReadCursor::new(&bytes); + + let resp = TunnelRespPkt::decode(&mut cur).map_err(|_| Error::new("TunnelDecode", GwErrorKind::Decode))?; + if resp.status_code != 0 { + return Err(Error::new("Tunnel", GwErrorKind::Connect)); + } + assert!(cur.eof()); + if !resp.consent_msg.is_empty() { + return Err(Error::new( + "Received consent message but showing it not implemented", + GwErrorKind::UnsupportedFeature, + )); + } + Ok(()) + } + + async fn tunnel_auth(&mut self) -> Result<(), Error> { + let req = TunnelAuthPkt { + fields_present: 0, + client_name: self.client_name.clone(), + }; + self.send_packet(&req).await?; + + let (_hdr, bytes) = self.read_packet().await?; + let mut cur = ReadCursor::new(&bytes); + let resp: TunnelAuthRespPkt = + TunnelAuthRespPkt::decode(&mut cur).map_err(|_| Error::new("TunnelAuth", GwErrorKind::Decode))?; + + if resp.error_code() != 0 { + return Err(Error::new("TunnelAuth", GwErrorKind::Connect)); + } + Ok(()) + } + + async fn channel(&mut self) -> Result { + let req = ChannelPkt { + resources: vec![self.target.server.clone()], + port: 3389, + protocol: 3, + }; + self.send_packet(&req).await?; + + let (hdr, bytes) = self.read_packet().await?; + assert!(hdr.ty == PktTy::ChannelResp); + let mut cur: ReadCursor<'_> = ReadCursor::new(&bytes); + let resp: ChannelResp = + ChannelResp::decode(&mut cur).map_err(|_| Error::new("ChannelResp", GwErrorKind::Decode))?; + if resp.error_code() != 0 { + return Err(Error::new("ChannelCreate", GwErrorKind::Connect)); + } + assert!(cur.eof()); + Ok(resp) + } +} + +impl AsyncRead for GwClient { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut core::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + // Propagate error or premature exit (?) + match self.work.poll_unpin(cx) { + Poll::Ready(Err(e)) => return Poll::Ready(Err(io::Error::other(e))), + Poll::Ready(Ok(Err(e))) => return Poll::Ready(Err(io::Error::other(e))), + Poll::Ready(_) => return Poll::Ready(Err(io::Error::other("Premature Work Task end?"))), + _ => (), + } + + // Get new bufs + if let Poll::Ready(Some(new_buf)) = self.rx.poll_recv(cx) { + self.rx_bufs.push(new_buf); + } + + // Read from all queued bufs + let mut n = 0; + self.rx_bufs.retain_mut(|rx_buf| { + let rem = buf.remaining(); + if rem == 0 { + return true; + } + let max = core::cmp::min(rem, rx_buf.len()); + buf.put_slice(&rx_buf[..max]); + n += max; + let _ = rx_buf.split_to(max); + + !rx_buf.is_empty() + }); + + if n > 0 { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } + } +} + +impl AsyncWrite for GwClient { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut core::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + // Propagate error or premature exit (?) + match self.work.poll_unpin(cx) { + Poll::Ready(Err(e)) => return Poll::Ready(Err(io::Error::other(e))), + Poll::Ready(Ok(Err(e))) => return Poll::Ready(Err(io::Error::other(e))), + Poll::Ready(_) => return Poll::Ready(Err(io::Error::other("Premature Work Task end?"))), + Poll::Pending => (), + } + + match self.tx.poll_reserve(cx) { + Poll::Ready(Ok(())) => { + if self.tx.send_item(Bytes::from(buf.to_vec())).is_err() { + return Poll::Ready(Err(io::Error::other("Sender closed"))); + } + return Poll::Ready(Ok(buf.len())); + } + Poll::Ready(Err(err)) => { + return Poll::Ready(Err(io::Error::other(err))); + } + Poll::Pending => (), + } + + Poll::Pending + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut core::task::Context<'_>) -> Poll> { + // TODO: call flush on the backing sink (e.g. websocket, but atleast for that backend doesnt seem to matter)? + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut core::task::Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/crates/ironrdp-mstsgu/src/macros.rs b/crates/ironrdp-mstsgu/src/macros.rs new file mode 100644 index 00000000..bb2299a6 --- /dev/null +++ b/crates/ironrdp-mstsgu/src/macros.rs @@ -0,0 +1,7 @@ +/// Creates a [`crate::Error`] with `Custom` kind and a source error attached to it +#[macro_export] +macro_rules! custom_err { + ( $context:expr, $source:expr $(,)? ) => {{ + <$crate::Error as $crate::GwErrorExt>::custom($context, $source) + }}; +} diff --git a/crates/ironrdp-mstsgu/src/proto.rs b/crates/ironrdp-mstsgu/src/proto.rs new file mode 100644 index 00000000..4efcf08d --- /dev/null +++ b/crates/ironrdp-mstsgu/src/proto.rs @@ -0,0 +1,600 @@ +use bitflags::bitflags; +use ironrdp_core::{ + cast_int, cast_length, ensure_fixed_part_size, ensure_size, unsupported_value_err, Decode, Encode, ReadCursor, + WriteCursor, +}; + +bitflags! { + /// 2.2.5.3.2 HTTP_EXTENDED_AUTH Enumeration + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub(crate) struct HttpExtendedAuth: u16 { + const HTTP_EXTENDED_AUTH_NONE = 0x01; + const HTTP_EXTENDED_AUTH_SC = 0x01; + const HTTP_EXTENDED_AUTH_PAA = 0x02; + const HTTP_EXTENDED_AUTH_SSPI_NTLM = 0x04; + } +} + +/// 2.2.5.3.3 HTTP_PACKET_TYPE Enumeration +#[repr(u16)] +#[derive(Eq, PartialEq, Copy, Clone, Debug, Default)] +pub(crate) enum PktTy { + #[default] + Invalid, + HandshakeReq = 0x01, + HandshakeResp = 0x02, + ExtendedAuth = 0x03, + TunnelCreate = 0x04, + TunnelResp = 0x05, + TunnelAuth = 0x06, + TunnelAuthResponse = 0x07, + ChannelCreate = 0x08, + ChannelResp = 0x09, + ChannelClose = 0x10, + Data = 0x0A, + ServiceMessage = 0x0B, + ReauthMessage = 0x0C, + Keepalive = 0x0D, +} + +impl PktTy { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +impl TryFrom for PktTy { + type Error = (); + + fn try_from(val: u16) -> Result { + let mapped = match val { + 0x01 => PktTy::HandshakeReq, + 0x02 => PktTy::HandshakeResp, + 0x03 => PktTy::ExtendedAuth, + 0x04 => PktTy::TunnelCreate, + 0x05 => PktTy::TunnelResp, + 0x06 => PktTy::TunnelAuth, + 0x07 => PktTy::TunnelAuthResponse, + 0x08 => PktTy::ChannelCreate, + 0x09 => PktTy::ChannelResp, + 0x0A => PktTy::Data, + 0x0B => PktTy::ServiceMessage, + 0x0C => PktTy::ReauthMessage, + 0x0D => PktTy::Keepalive, + 0x10 => PktTy::ChannelClose, + _ => return Err(()), + }; + Ok(mapped) + } +} + +/// 2.2.10.9 HTTP_PACKET_HEADER Structure +#[derive(Default, Debug)] +pub(crate) struct PktHdr { + pub ty: PktTy, + pub _reserved: u16, + pub length: u32, +} + +impl PktHdr { + const FIXED_PART_SIZE: usize = 4 /* ty */ + 2 /* _reserved */ + 2 /* length */; +} + +impl Encode for PktHdr { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.ty.as_u16()); + dst.write_u16(self._reserved); + dst.write_u32(self.length); + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_PACKET_HEADER" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for PktHdr { + fn decode(src: &mut ReadCursor<'a>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + let ty = src.read_u16(); + let mty = PktTy::try_from(ty).map_err(|_| unsupported_value_err("PktHdr::ty", "ty", format!("0x{ty:x}")))?; + + Ok(PktHdr { + ty: mty, + _reserved: src.read_u16(), + length: src.read_u32(), + }) + } +} + +/// 2.2.10.10 HTTP_HANDSHAKE_REQUEST_PACKET Structure +#[derive(Default)] +pub(crate) struct HandshakeReqPkt { + pub ver_major: u8, + pub ver_minor: u8, + pub client_version: u16, + pub extended_auth: HttpExtendedAuth, +} + +impl Encode for HandshakeReqPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::HandshakeReq, + length: u32::try_from(self.size()).expect("handshake packet size fits in u32"), + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u8(self.ver_major); + dst.write_u8(self.ver_minor); + dst.write_u16(self.client_version); + dst.write_u16(self.extended_auth.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_HANDSHAKE_REQUEST_PACKET" + } + + fn size(&self) -> usize { + PktHdr::FIXED_PART_SIZE + 6 + } +} + +/// 2.2.10.11 HTTP_HANDSHAKE_RESPONSE_PACKET Structure +#[derive(Debug)] +pub(crate) struct HandshakeRespPkt { + pub error_code: u32, + pub ver_major: u8, + pub ver_minor: u8, + pub server_version: u16, + pub _extended_auth: HttpExtendedAuth, +} + +impl HandshakeRespPkt { + const FIXED_PART_SIZE: usize = 4 /* error_code */ + 1 /* ver_major */ + 1 /* ver_minor */ + 2 /* server_auth */ + 1 /*extended_auth*/; +} + +impl Decode<'_> for HandshakeRespPkt { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + Ok(HandshakeRespPkt { + error_code: src.read_u32(), + ver_major: src.read_u8(), + ver_minor: src.read_u8(), + server_version: src.read_u16(), + _extended_auth: { + let raw = src.read_u16(); + HttpExtendedAuth::from_bits(raw) + .ok_or_else(|| unsupported_value_err("HandshakeResp", "extended_auth", format!("0x{raw:x}")))? + }, + }) + } +} + +/// 2.2.10.18 HTTP_TUNNEL_PACKET +#[derive(Default)] +pub(crate) struct TunnelReqPkt { + pub caps: u32, + pub fields_present: u16, + pub _reserved: u16, +} + +impl Encode for TunnelReqPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::TunnelCreate, + length: u32::try_from(self.size()).expect("tunnel request packet size fits in u32"), + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u32(self.caps); + dst.write_u16(self.fields_present); + dst.write_u16(self._reserved); + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_TUNNEL_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 8 + } +} + +/// 2.2.5.3.9 HTTP_CAPABILITY_TYPE Enumeration +#[repr(u32)] +#[expect(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum HttpCapsTy { + QuarSOH = 1, + IdleTimeout = 2, + MessagingConsentSign = 4, + MessagingServiceMsg = 8, + Reauth = 0x10, + UdpTransport = 0x20, +} + +impl HttpCapsTy { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub(crate) fn as_u32(self) -> u32 { + self as u32 + } +} + +/// 2.2.5.3.8 HTTP_TUNNEL_RESPONSE_FIELDS_PRESENT_FLAGS +#[repr(u16)] +#[derive(Copy, Clone)] +enum HttpTunnelResponseFields { + TunnelID = 1, + Caps = 2, + /// nonce & server_cert + Soh = 4, + Consent = 0x10, +} + +impl HttpTunnelResponseFields { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +/// 2.2.10.20 HTTP_TUNNEL_RESPONSE Structure +#[derive(Debug, Default)] +pub(crate) struct TunnelRespPkt { + pub _server_version: u16, + pub status_code: u32, + pub fields_present: u16, + pub _reserved: u16, + + // 2.2.10.21 HTTP_TUNNEL_RESPONSE_OPTIONAL + pub tunnel_id: Option, + pub caps_flags: Option, + pub nonce: Option, + pub server_cert: Vec, + pub consent_msg: Vec, +} + +impl TunnelRespPkt { + const FIXED_PART_SIZE: usize = 2 /* server_version */ + 4 /* status_code */ + 2 /* fields_present */ + 2 /* reserved */; +} + +impl Decode<'_> for TunnelRespPkt { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + let mut pkt = TunnelRespPkt { + _server_version: src.read_u16(), + status_code: src.read_u32(), + fields_present: src.read_u16(), + _reserved: src.read_u16(), + ..TunnelRespPkt::default() + }; + + if pkt.fields_present & (HttpTunnelResponseFields::TunnelID.as_u16()) != 0 { + ensure_size!(in: src, size: 4); + pkt.tunnel_id = Some(src.read_u32()); + } + if pkt.fields_present & (HttpTunnelResponseFields::Caps.as_u16()) != 0 { + ensure_size!(in: src, size: 4); + pkt.caps_flags = Some(src.read_u32()); + } + if pkt.fields_present & (HttpTunnelResponseFields::Soh.as_u16()) != 0 { + ensure_size!(in: src, size: 2 + 2); + pkt.nonce = Some(src.read_u16()); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + pkt.server_cert = src.read_slice(len).to_vec(); + } + if pkt.fields_present & (HttpTunnelResponseFields::Consent.as_u16()) != 0 { + ensure_size!(in: src, size: 2); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + pkt.consent_msg = src.read_slice(len).to_vec(); + } + + Ok(pkt) + } +} + +/// 2.2.10.7 HTTP_EXTENDED_AUTH_PACKET Structure +pub(crate) struct ExtendedAuthPkt { + error_code: u32, + blob: Vec, +} + +impl Encode for ExtendedAuthPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::ExtendedAuth, + length: cast_int!("packet length", self.size())?, + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u32(self.error_code); + let blob_len: u16 = cast_int!("blob length", self.blob.len())?; + dst.write_u16(blob_len); + dst.write_slice(&self.blob); + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_EXTENDED_AUTH_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 6 + self.blob.len() + } +} + +impl Decode<'_> for ExtendedAuthPkt { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_size!(in: src, size: 4 + 2); + let error_code = src.read_u32(); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + + Ok(ExtendedAuthPkt { + error_code, + blob: src.read_slice(len).to_vec(), + }) + } +} + +/// 2.2.10.14 HTTP_TUNNEL_AUTH_PACKET Structure +pub(crate) struct TunnelAuthPkt { + pub fields_present: u16, + pub client_name: String, +} + +impl Encode for TunnelAuthPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::TunnelAuth, + length: cast_int!("packet length", self.size())?, + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u16(self.fields_present); + + let client_name_len = self.client_name.encode_utf16().count() * 2 + 2; // Add 2 to account for a null terminator (0x0000). + let client_name_len: u16 = cast_int!("client name length", client_name_len)?; + dst.write_u16(client_name_len); + + for c in self.client_name.encode_utf16() { + dst.write_u16(c); + } + + dst.write_u16(0); + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_TUNNEL_AUTH_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 4 + 2 * (self.client_name.len() + 1) + } +} + +/// 2.2.10.16 HTTP_TUNNEL_AUTH_RESPONSE Structure +#[derive(Debug)] +pub(crate) struct TunnelAuthRespPkt { + error_code: u32, + _fields_present: u16, + _reserved: u16, +} + +impl TunnelAuthRespPkt { + const FIXED_PART_SIZE: usize = 4 /* error_code */ + 2 /* fields_present */ + 2 /* _reserved */; + + pub(crate) fn error_code(&self) -> u32 { + self.error_code + } +} + +impl Decode<'_> for TunnelAuthRespPkt { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + Ok(TunnelAuthRespPkt { + error_code: src.read_u32(), + _fields_present: src.read_u16(), + _reserved: src.read_u16(), + }) + } +} + +/// 2.2.10.2 HTTP_CHANNEL_PACKET +pub(crate) struct ChannelPkt { + pub resources: Vec, + pub port: u16, + pub protocol: u16, +} + +impl Encode for ChannelPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::ChannelCreate, + length: cast_int!("packet length", self.size())?, + ..PktHdr::default() + }; + hdr.encode(dst)?; + + let resources_count: u8 = cast_length!("resources count", self.resources.len())?; + dst.write_u8(resources_count); + dst.write_u8(0); // alt_names + dst.write_u16(self.port); + dst.write_u16(self.protocol); + + // 2.2.10.3 HTTP_CHANNEL_PACKET_VARIABLE + for res in &self.resources { + let res_utf16_len = res.encode_utf16().count() * 2 + 2; // Add 2 to account for a null terminator (0x0000). + let res_len: u16 = cast_int!("resource name UTF-16 length", res_utf16_len)?; + dst.write_u16(res_len); + for b in res.encode_utf16() { + dst.write_u16(b); + } + dst.write_u16(0); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_CHANNEL_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 6 + self.resources.iter().map(|x| 2 + 2 * (x.len() + 1)).sum::() + } +} + +/// 2.2.10.4 HTTP_CHANNEL_RESPONSE +#[derive(Default, Debug)] +pub(crate) struct ChannelResp { + error_code: u32, + fields_present: u16, + _reserved: u16, + + /// 2.2.10.5 HTTP_CHANNEL_RESPONSE_OPTIONAL + chan_id: Option, + udp_port: u16, + authn_cookie: Vec, +} + +impl ChannelResp { + const FIXED_PART_SIZE: usize = 4 /* error_code */ + 2 /* fields_present */ + 2 /* _reserved */; + + pub(crate) fn error_code(&self) -> u32 { + self.error_code + } +} + +impl Decode<'_> for ChannelResp { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + let mut resp = ChannelResp { + error_code: src.read_u32(), + fields_present: src.read_u16(), + _reserved: src.read_u16(), + ..ChannelResp::default() + }; + if resp.fields_present & 1 != 0 { + ensure_size!(in: src, size: 4); + resp.chan_id = Some(src.read_u32()); + } + if resp.fields_present & 2 != 0 { + ensure_size!(in: src, size: 2); + resp.udp_port = src.read_u16(); + } + if resp.fields_present & 4 != 0 { + ensure_size!(in: src, size: 2); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + resp.authn_cookie = src.read_slice(len).to_vec(); + } + Ok(resp) + } +} + +/// 2.2.10.6 HTTP_DATA_PACKET +pub(crate) struct DataPkt<'a> { + pub data: &'a [u8], +} + +impl Encode for DataPkt<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::Data, + length: cast_int!("packet length", self.size())?, + ..PktHdr::default() + }; + hdr.encode(dst)?; + let data_len: u16 = cast_int!("data payload length", self.data.len())?; + dst.write_u16(data_len); + dst.write_slice(self.data); + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_DATA_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 2 + self.data.len() + } +} + +impl<'a> Decode<'a> for DataPkt<'a> { + fn decode(src: &mut ReadCursor<'a>) -> ironrdp_core::DecodeResult { + ensure_size!(in: src, size: 2); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + Ok(DataPkt { + data: src.read_slice(len), + }) + } +} + +pub(crate) struct KeepalivePkt; + +impl Encode for KeepalivePkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + let hdr = PktHdr { + ty: PktTy::Keepalive, + length: u32::try_from(self.size()).expect("keepalive packet size fits in u32"), + ..PktHdr::default() + }; + hdr.encode(dst) + } + + fn name(&self) -> &'static str { + "KEEPALIVE" + } + + fn size(&self) -> usize { + PktHdr::default().size() + } +} diff --git a/crates/ironrdp-pdu-generators/Cargo.toml b/crates/ironrdp-pdu-generators/Cargo.toml new file mode 100644 index 00000000..c867edb3 --- /dev/null +++ b/crates/ironrdp-pdu-generators/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ironrdp-pdu-generators" +version = "0.0.0" +description = "`proptest` generators for `ironrdp-pdu` types" +publish = false +edition.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +# ironrdp-pdu.workspace = true +# proptest.workspace = true + +[lints] +workspace = true + diff --git a/crates/ironrdp-pdu-generators/README.md b/crates/ironrdp-pdu-generators/README.md new file mode 100644 index 00000000..6845d079 --- /dev/null +++ b/crates/ironrdp-pdu-generators/README.md @@ -0,0 +1,7 @@ +# IronRDP PDU generators + +`proptest` generators for `ironrdp-pdu` types. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-pdu-generators/src/lib.rs b/crates/ironrdp-pdu-generators/src/lib.rs new file mode 100644 index 00000000..df55d21d --- /dev/null +++ b/crates/ironrdp-pdu-generators/src/lib.rs @@ -0,0 +1,8 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +// No need to be as strict as in production libraries +#![allow(clippy::arithmetic_side_effects)] +#![allow(clippy::cast_lossless)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_possible_wrap)] +#![allow(clippy::cast_sign_loss)] diff --git a/crates/ironrdp-pdu/CHANGELOG.md b/crates/ironrdp-pdu/CHANGELOG.md new file mode 100644 index 00000000..ba7ae8cd --- /dev/null +++ b/crates/ironrdp-pdu/CHANGELOG.md @@ -0,0 +1,90 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-pdu-v0.5.0...ironrdp-pdu-v0.6.0)] - 2025-08-29 + +### Features + +- Implement `Default` trait on `ExtendedClientOptionalInfoBuilder` (#891) ([ae052ed835](https://github.com/Devolutions/IronRDP/commit/ae052ed83598ad1f4ad7038b153e3c5398d2a738)) + +### Bug Fixes + +- [**breaking**] Update timezone info to use i32 bias (#921) ([119c7077c9](https://github.com/Devolutions/IronRDP/commit/119c7077c98e4b43021619378c4f251c1f95ae17)) + + Switches `bias` from an unsigned to a signed integer. + This matches the updated specification from Microsoft. + +### Build + +- Bump thiserror to 2.0 ([b4fb0aa0c7](https://github.com/Devolutions/IronRDP/commit/b4fb0aa0c79aa409d1b6a5f43ab23448eede4e51)) + +- Bump der-parser to 10.0 ([03cac54ada](https://github.com/Devolutions/IronRDP/commit/03cac54ada50fae13d085b855a9b8db37d615ba8)) + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-pdu-v0.4.0...ironrdp-pdu-v0.5.0)] - 2025-05-27 + +### Features + +- Make client_codecs_capabilities() configurable ([783702962a](https://github.com/Devolutions/IronRDP/commit/783702962a2e842f9d5046ac706048ba124e1401)) + +- BitmapCodecs struct ([f03ee393a3](https://github.com/Devolutions/IronRDP/commit/f03ee393a36906114b5bcba0e88ebc6869a99785)) + +### Bug Fixes + +- Fix possible out of bound indexing in RFX module (#724) ([9f4e6d410b](https://github.com/Devolutions/IronRDP/commit/9f4e6d410b631d8a6b0c09c2abc0817a83cf042b)) + + An index bound check was missing in the RFX module. Found by fuzzer. + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-pdu-v0.3.1...ironrdp-pdu-v0.4.0)] - 2025-03-12 + +### Bug Fixes + +- TS_RFX_CHANNELT width/height SHOULD be within range ([097cdb66f9](https://github.com/Devolutions/IronRDP/commit/097cdb66f965700caeea5659ff7fe4a129b84838)) + + According to the specification, the value does not need to be in the range: + https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdprfx/4060f07e-9d73-454d-841e-131a93aca675 + + (the ironrdp-server can send larger values) + +### Refactor + +- [**breaking**] Remove RfxChannelWidth and RfxChannelHeight structs ([7cb1ac99d1](https://github.com/Devolutions/IronRDP/commit/7cb1ac99d189cdcaa17fa17e51f95be630e9982e)) + +## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-pdu-v0.3.0...ironrdp-pdu-v0.3.1)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-pdu-v0.2.0...ironrdp-pdu-v0.3.0)] - 2025-03-07 + +### Bug Fixes + +- Make AddressFamily parsing resilient (#672) ([6b4af94071](https://github.com/Devolutions/IronRDP/commit/6b4af94071bfb0adff482cc33b75e6c37ff6e10f)) + +- Fix FastPathHeader minimal size (#687) ([3b9d558e9c](https://github.com/Devolutions/IronRDP/commit/3b9d558e9c958297d9654861df515e2a8658bf8b)) + + The minimal_size() logic didn't properly take into account the overall + PDU size. + + This fixes random error/disconnect in client. + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-pdu-v0.1.2...ironrdp-pdu-v0.2.0)] - 2025-01-28 + +### Features + +- ClientLicenseInfo and other license PDU-related adjustments (#634) ([dd221bf224](https://github.com/Devolutions/IronRDP/commit/dd221bf22401c4635798ec012724cba7e6d503b2)) + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-pdu-v0.1.1...ironrdp-pdu-v0.1.2)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-pdu/Cargo.toml b/crates/ironrdp-pdu/Cargo.toml new file mode 100644 index 00000000..cbe31ad8 --- /dev/null +++ b/crates/ironrdp-pdu/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "ironrdp-pdu" +version = "0.6.0" +readme = "README.md" +description = "RDP PDU encoding and decoding" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +# test = false + +[features] +default = [] +std = ["alloc", "ironrdp-error/std", "ironrdp-core/std"] +alloc = ["ironrdp-core/alloc", "ironrdp-error/alloc"] +qoi = [] +qoiz = ["qoi"] + +[dependencies] +bitflags = "2.9" +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["std"] } # public +ironrdp-error = { path = "../ironrdp-error", version = "0.1" } # public +tap = "1" + +# TODO: get rid of these dependencies (related code should probably go into another crate) +bit_field = "0.10" +byteorder = "1.5" # TODO: remove +der-parser = "10.0" +thiserror = "2.0" +md5 = { package = "md-5", version = "0.10" } +num-bigint = "0.4" +num-derive.workspace = true # TODO: remove +num-integer = "0.1" +num-traits.workspace = true # TODO: remove +sha1 = "0.10" +x509-cert = { version = "0.2", default-features = false, features = ["std"] } +pkcs1 = "0.7" + +[dev-dependencies] +expect-test.workspace = true + +[lints] +workspace = true diff --git a/crates/ironrdp-pdu/LICENSE-APACHE b/crates/ironrdp-pdu/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-pdu/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-pdu/LICENSE-MIT b/crates/ironrdp-pdu/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-pdu/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-pdu/README.md b/crates/ironrdp-pdu/README.md new file mode 100644 index 00000000..256194ed --- /dev/null +++ b/crates/ironrdp-pdu/README.md @@ -0,0 +1,477 @@ +# IronRDP PDU + +RDP PDU encoding and decoding library. + +- [Overview of encoding and decoding traits](#overview-of-encoding-and-decoding-traits) +- [Difference between `WriteBuf` and `WriteCursor`](#difference-between-writebuf-and-writecursor) +- [Difference between `WriteBuf` and `Vec`](#difference-between-writebuf-and-vecu8) +- [Most PDUs are "plain old data" structures with public fields](#most-pdus-are-plain-old-data-structures-with-public-fields) +- [Enumeration-like types should allow resilient parsing](#enumeration-like-types-should-allow-resilient-parsing) +- [On bit flags](#on-bit-flags) + +## Overview of encoding and decoding traits + +It’s important for `Encode` to be object-safe in order to enable patterns such as the one +found in `ironrdp-svc`: + +```rust +pub trait SvcProcessor { + fn process(&mut self, payload: &[u8]) -> PduResult>>; +} +``` + +(The actual trait is a bit more complicated, but this gives the rough idea.) + +TODO: elaborate this section + +## Difference between `WriteBuf` and `WriteCursor` + +`WriteCursor` is a wrapper around `&mut [u8]` and its purpose is to: + +- Provide convenient methods such as `write_u8`, `write_u16`, `write_u16_be`, etc. +- Guarantee syscall-free, infallible write access to a continuous slice of memory. +- Keep track of the number of bytes written. +- Allow backtracking to override a value previously written or skipped. +- Be `no-std` and `no-alloc` friendly, which `std::io::Cursor` is not as of today. + +The underlying storage could be abstracted over, but it’s deliberately hardcoded to `&mut [u8]` +so traits such as `Encode` using `WriteCursor` in their associated methods are object-safe. + +`WriteBuf` is used in APIs where the required space cannot be known in advance. For instance, +`ironrdp_connector::Sequence::step` is taking `&mut WriteBuf` instead of `&mut +WriteCursor` because it’s unlikely the user knows exactly how much space is required to encode a +specific response before actually processing the input payload, and as such there is no easy way +to communicate the caller how much space is required before calling the function. + +Consider this piece of code: + +```rust +fn process(&mut self, payload: &[u8], output: &mut WriteBuf) -> PduResult<()> { + let server_request: ServerRequest = ironrdp_pdu::decode(payload)?; + + match server_request.order { + ServerOrder::DoThis => { + // do this + let response = DoThisResponse { /* … */ }; + + // buffer is grown, or not, as appropriate, and `DoThisResponse` is encoded in the "unfilled" region + ironrdp_pdu::encode_buf(response, output)?; + } + ServerOrder::DoThat => { + // do that + let response = DoThatResponse { /* … */ }; + + // same as above + ironrdp_pdu::encode_buf(response, output)?; + } + } + + Ok(()) +} +``` + +Methods such as `write_u8` are overlapping with the `WriteCursor` API, but it’s mostly for +convenience if one needs to manually write something in an ad-hoc fashion, and using `WriteCursor` +is preferred in order to write `no-std` and `no-alloc` friendly code. + +## Difference between `WriteBuf` and `Vec` + +`WriteBuf` roles include: + +- Maintaining a non-trivial piece of initialized memory consistently, all while ensuring that the + internal `Vec` doesn't grow excessively large in memory throughout the program's + execution. Keeping a piece of initialized memory around is useful to amortize the initialization + cost of `Vec::resize` when building a `WriteCursor` (which requires a mutable slice of + initialized memory, `&mut [u8]`). + +- Keep track of the filled region in order to easily write multiple items sequentially within the + same buffer. + +`WriteCursor`, in essence, is a helper for this kind of code: + +```rust +pub fn encode_buf(pdu: &T, buf: &mut Vec, filled_len: usize) -> PduResult { + let pdu_size = pdu.size(); + + // Resize the buffer, making sure there is enough space to fit the serialized PDU + if buf.len() < pdu_size { + buf.resize(filled_len + pdu_size, 0); + } + + // Proceed to actually serialize the PDU into the buffer… + let mut cursor = WriteCursor::new(&mut buf[filled_len..]); + encode_cursor(pdu, &mut cursor)?; + + let written = cursor.pos(); + + Ok(written) +} + +fn somewhere_else() -> PduResult<()> { + let mut state_machine = /* … */; + let mut buf = Vec::new(); + let mut filled_len; + + while !state_machine.is_terminal() { + let pdus = state_machine.step(); + + filled_len = 0; + buf.shrink_to(16384); // Maintain up to 16 kib around + + for pdu in pdus { + filled_len += encode_buf(&pdu, &mut buf, filled_len)?; + } + + let filled = &buf[..filled_len]; + + // Do something with `filled` + } +} +``` + +Observe that this code, which relies on `Vec`, demands extra bookkeeping for the filled region +and a cautious approach when clearing and resizing it. + +In comparison, the same code using `WriteBuf` looks like this: + +```rust +pub fn encode_buf(pdu: &T, buf: &mut WriteBuf) -> PduResult { + let pdu_size = pdu.size(); + + let dst = buf.unfilled_to(pdu_size); + + let mut cursor = WriteCursor::new(dst); + encode_cursor(pdu, &mut cursor)?; + + let written = cursor.pos(); + buf.advance(written); + + Ok(written) +} + +fn somewhere_else() -> PduResult<()> { + let mut state_machine = /* … */; + let mut buf = WriteBuf::new(); + + while !state_machine.is_terminal() { + let pdus = state_machine.step(); + + buf.clear(); + + for pdu in pdus { + encode_buf(&pdu, &mut buf)?; + } + + let filled = buf.filled(); + + // Do something with `filled` + } +} +``` + +A big enough mutable slice of the unfilled region is retrieved by using the `unfilled_to` helper +method (allocating more memory as necessary), a `WriteCursor` wrapping this slice is created and +used to actually encode the PDU. The filled region cursor of the `WriteBuf` is moved forward so that +the `filled` method returns the right region, and subsequent calls to `encode_buf` do not override +it until `clear` is called. + +## Most PDUs are "plain old data" structures with public fields + +In general, it is desirable for library users to be able to manipulate the fields directly because: + +- One can deconstruct the types using pattern matching +- One can construct the type using a [field struct expression][1] (which is arguably more readable + than a `new` method taking tons of parameters, and less boilerplate than a builder) +- One can use the [struct (functional) update syntax][2] +- One can move fields out of the struct without us having to add additional `into_xxx` API for each + combination +- One can mutably borrow multiple fields of the struct at the same time (a getter will cause the + entire struct to be considered borrowed by the borrow checker) + +Keeping the fields private is opting out of all of this and making the API stiffer. Of course, +all of this applies because for most PDU structs there is no important invariants to uphold: they +are mostly dumb data holders or "plain old data structures" with no particular logic to run at +construction or destruction time. The story is not the same for objects with business logic (which +are mostly not part of `ironrdp-pdu`) + +When hiding some fields is really required, one of the following approach is suggested: +- a "[Builder Lite][3]" pattern, +- a "[Init Struct][4]" pattern, or +- a standard handcrafted Builder Pattern + +[1]: https://doc.rust-lang.org/reference/expressions/struct-expr.html#field-struct-expression +[2]: https://doc.rust-lang.org/reference/expressions/struct-expr.html#functional-update-syntax +[3]: https://matklad.github.io/2022/05/29/builder-lite.html +[4]: https://xaeroxe.github.io/init-struct-pattern/ + +## Enumeration-like types should allow resilient parsing + +The **TL;DR** is that enums in Rust should not be used when parsing resilience is required. + +Network protocols are generally designed with forward and backward compatibility in mind. Items +like status codes, error codes, and other enumeration-like types may later get extended to include +new values. Additionally, it's not uncommon for different implementations of the same protocol to +disagree with each other. Some implementations may exhibit a bug, inserting an unexpected value, +while others may intentionally extend the original protocol with a custom value. + +Therefore, implementations of such protocols should decode these into a more flexible data type +that can accommodate a broader range of values, even when working with a language like Rust. As +long as the unknown value is not critical and can be handled gracefully, the implementation should +make every effort not to fail, especially during parsing. + +For example, let’s consider the following `enum` in Rust: + +```rust +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MyNetworkCode { + FirstValue = 0, + SecondValue = 1, +} +``` + +This type cannot be used to hold new values that may be added to the protocol in the future. Thus, +it is not a suitable option. + +Many Rust developers will instinctively write the following instead: + +```rust +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MyNetworkCode { + FirstValue = 0, + SecondValue = 1, + // Fallback variant + Unknown(u32), +} +``` + +This type can indeed hold additional values in its fallback variant, and the implementation can +remain compatible with future protocol versions as desired. + +However, this solution is not without its issues. There is a hard to catch forward-compatibility +hazard at the library level: when a new value, such as `ThirdValue = 2`, is added to the enum, the +`Unknown` variant for this value will no longer be emitted by the library (the library does not +construct and return the value anymore). This can lead to _silent_ failures in downstream code that +relies on matching `Unknown` to handle the previously unknown value. + +For instance: + +```rust +// Library code + +impl MyNetworkCode { + fn parse_network_code(value: u32) -> MyNetworkCode { + match value { + 0 => MyNetworkCode::FirstValue, + 1 => MyNetworkCode::SecondValue, + _ => MyNetworkCode::Unknown(value), + } + } +} + +// User code + +fn handle_network_code(reader: /* … */) { + let value = reader.read_u32(); + let code = MyNetworkCode::from_u32(value); + + if code == MyNetworkCode::Unknown(2) { + // The library doesn’t know about this value yet, but we need to handle it because […] + } +} +``` + +Once the library is updated to handle the third value: + +```rust +// Library code + +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MyNetworkCode { + FirstValue = 0, + SecondValue = 1, + ThirdValue = 2, // NEW + Unknown(u32), +} + +impl MyNetworkCode { + fn parse_network_code(value: u32) -> MyNetworkCode { + match value { + 0 => MyNetworkCode::FirstValue, + 1 => MyNetworkCode::SecondValue, + 2 => MyNetworkCode::ThirdValue, // NEW + _ => MyNetworkCode::Unknown(value), // This branch is not entered anymore when value = 2 + } + } +} + +// User code (unchanged) + +fn handle_network_code(reader: /* … */) { + let value = reader.read_u32(); + let code = MyNetworkCode::from_u32(value); + + if code == MyNetworkCode::Unknown(2) { // <- The library does not construct this value as it used to… + // … the special case is not handled anymore; no warning and no error + // is emitted by the compiler, so it’s very easy to overlook this + } +} +``` + +Several other concerns arise: + +- `Unknown(2)` and `ThirdValue` are conceptually the same thing, but are represented differently in memory. +- The default `PartialEq` implementation that can be derived will return `false` when testing for + equality (i.e.: `Unknown(2) != ThirdValue`). Fixing this requires manual implementation of `PartialEq`. +- Even if `PartialEq` is fixed, the pattern matching issue can’t be fixed. +- The size of this type is bigger than necessary. + +All in all, this can be considered a potential source of bugs in code consuming the API, and the +bottom line is to avoid type definitions that allow for the same thing to be represented in two +different ways, i.e: different "type-level values", because the library will not return or "emit" +both at the same time; it’s as if one of the two value was implicitly "dead"[^deranged-note]. + +[^deranged-note]: Note that [`RangedU32::new_static`][RangedU32_new_static] from [`deranged`][deranged] +(ranged integers library) could help here, but in this case the end result is not ergonomic and still +error-prone as it’s natural to reach for [`RangedU32::get`][RangedU32_get] instead when comparing the +value. This approach would work if a lint was emitted when the compiler detects that the condition +operand will always evaluate to `false`. Built-in ranged integers would be great here. + +Another approach would be for `Unknown` not to hold the original value at all, ensuring it can never +overlap with another variant. In this case, users would need to wait for the library to be updated +before they can implement special handling for `ThirdValue`: + +```rust +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MyNetworkCode { + FirstValue = 0, + SecondValue = 1, + // Fallback variant; do not rely on this variant + Unknown, +} +``` + +However, there is no guarantee that users will not rely on `Unknown` being emitted anyway. This +approach merely discourages them from doing so by making the fallback variant much less powerful. + +The downside here is that one can no longer determine the original payload's value, and the "round- +trip" property is lost because this becomes destructive parsing, with no way to determine how the +structure should be (re)encoded. + +Overall, it’s not a very good option either. + +Instead, consider providing a newtype struct wrapping the appropriate integer type: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MyNetworkCode(u32); + +impl MyNetworkCode { + pub const FIRST_VALUE: Self = Self(0); + pub const SECOND_VALUE: Self = Self(1); +} +``` + +This is exactly [how the `http` crate deals with status codes][http-status-code]. + +This pattern is used in several places: + +- `FailureCode` in the `ironrdp_pdu::nego` module. +- `Code` in the `ironrdp_graphics::rle` module. +- `ClipboardFormatId` in the `ironrdp_cliprdr::pdu::format_list` module. + +Of course, the main drawback is that exhaustive matching is not possible. + +The question to consider is whether it's genuinely necessary to handle all the possible values +explicitly. It may not often be required or desirable. Since neither the client nor the server +should typically break when the protocol is extended, an older client (not handling the new value) +should generally be able to function properly with a newer server, and vice versa. In other words, +not handling the new value must not be an immediate problem. However, it is often desirable to +handle a subset of possible values or to use it for logging or metric purposes. For these purposes, +it’s actually fine to not do exhaustive matching, and ability to work with unknown values is useful +(e.g.: logging unknown values). + +Yet, it can be useful to provide a pattern-matching-friendly API in some cases. In such situations, +it is advisable to also offer a higher-level abstraction, such as an enum, that can be obtained from +the "raw" `MyNetworkCode` value. This pattern is applied in the `ironrdp-rdcleanpath` crate to convert +from a lower-level, future-proof, resilient but awkward-to-use type, `RDCleanPathPdu`, to a high-level, +easier-to-use type, `RDCleanPath`. + +Don’t forget that in some cases, the protocol specification explicitly states that other values +MUST always be rejected. In such cases, an `enum` is deemed appropriate. + +[RangedU32_new_static]: https://docs.rs/deranged/0.3.8/deranged/struct.RangedU32.html#method.new_static +[RangedU32_get]: https://docs.rs/deranged/0.3.8/deranged/struct.RangedU32.html#method.get +[deranged]: https://docs.rs/deranged/0.3.8/ +[http-status-code]: https://docs.rs/http/0.2.9/http/status/struct.StatusCode.html + +## On bit flags + +The **TL;DR** is: + +- Use **both** `from_bits_retain` and `const _ = !0` when resilient parsing is required. + - `const _ = !0` ensures we don’t accidentally have non resilient or destructive parsing. In + addition to that, generated methods such as `complement` (`!`) will consider additional bits + and follow the principle of least surprise (`!!flags == flags`). + - `from_bits_retain` makes it clear at the call site that preserving all the bits is intentional. +- Use `from_bits` WITHOUT `const _ = !0` when strictness is required (almost never in IronRDP), and + document why with an in-source comment. + +Bit flags are used quite pervasively in the RDP protocol. +IronRDP is relying on the [`bitflags` crate][bitflags] to generate well-defined flags structures, +freeing us from worrying about bitwise logic. + +Three notable methods generated by the `bitflags` crate are: + +- [`from_bits`][from_bits], +- [`from_bits_truncate`][from_bits_truncate], and +- [`from_bits_retain`][from_bits_retain]. + +Within IronRDP codebase, `from_bits_retain` should generally be used over `from_bits`, and +`from_bits_truncate` is likely wrong. `from_bits_retain` will simply ignore unknown bits, but will +not unset them (unlike `from_bits_truncate`), i.e.: the underlying `u32` is set exactly to the value +received from the network. + +Rationale is: + +- PDU decoding and encoding logic should uphold the round-tripping property (`m = encode(decode(m))`), + and for this property to hold, parsing must be non-destructive (i.e.: lossless), + but `from_bits_truncate` is destructive (unknown bits are discarded). +- Resilient parsing is generally preferred, ignoring unknown values as long as they are not needed and/or as + long as ignoring them is causing no harm, but `from_bits` is not lenient + +Note that it’s okay to use `from_bits` if strictness is actually required somewhere, but such places must be +documented with a comment explaining why refusing unknown flags is better. + +`bitflags` v2.4 also introduced a new syntax in the `bitflags!` macro (): + +```rust +bitflags! { + pub struct Flags: u32 { + const A = 0b00000001; + const B = 0b00000010; + const C = 0b00000100; + + // The source may set any bits + const _ = !0; // <- This + } +} +``` + +There is crate-level documentation for this: + +This addition makes `from_bits_truncate` behave exactly like `from_bits_retain`, because all values +are considered to be known and defined. In such cases, `from_bits` also never fails therefore works +precisely the same as `from_bits_retain`, except it’s less ergonomic because it returns a `Result` +which must be needlessly handled. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP +[bitflags]: https://crates.io/crates/bitflags +[from_bits]: https://docs.rs/bitflags/2.4.0/bitflags/example_generated/struct.Flags.html#method.from_bits +[from_bits_truncate]: https://docs.rs/bitflags/2.4.0/bitflags/example_generated/struct.Flags.html#method.from_bits_truncate +[from_bits_retain]: https://docs.rs/bitflags/2.4.0/bitflags/example_generated/struct.Flags.html#method.from_bits_retain diff --git a/crates/ironrdp-pdu/src/basic_output/bitmap/mod.rs b/crates/ironrdp-pdu/src/basic_output/bitmap/mod.rs new file mode 100644 index 00000000..f1bd153a --- /dev/null +++ b/crates/ironrdp-pdu/src/basic_output/bitmap/mod.rs @@ -0,0 +1,281 @@ +#[cfg(test)] +mod tests; + +pub mod rdp6; + +use core::fmt::{self, Debug}; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; + +use crate::geometry::InclusiveRectangle; + +const FIRST_ROW_SIZE_VALUE: u16 = 0; + +/// TS_UPDATE_BITMAP_DATA +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BitmapUpdateData<'a> { + pub rectangles: Vec>, +} + +impl BitmapUpdateData<'_> { + const NAME: &'static str = "TS_UPDATE_BITMAP_DATA"; + const FIXED_PART_SIZE: usize = 2 /* flags */ + 2 /* nrect */; +} + +impl BitmapUpdateData<'_> { + pub fn encode_header(rectangles: u16, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: 2); + + dst.write_u16(BitmapFlags::BITMAP_UPDATE_TYPE.bits()); + dst.write_u16(rectangles); + + Ok(()) + } +} + +impl Encode for BitmapUpdateData<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let rectangle_count = cast_length!("number of rectangles", self.rectangles.len())?; + + Self::encode_header(rectangle_count, dst)?; + + for bitmap_data in self.rectangles.iter() { + bitmap_data.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.rectangles + .iter() + .fold(Self::FIXED_PART_SIZE, |size, new| size + new.size()) + } +} + +impl<'de> Decode<'de> for BitmapUpdateData<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let update_type = BitmapFlags::from_bits_truncate(src.read_u16()); + if !update_type.contains(BitmapFlags::BITMAP_UPDATE_TYPE) { + return Err(invalid_field_err!("updateType", "invalid update type")); + } + + let rectangle_count = usize::from(src.read_u16()); + let mut rectangles = Vec::with_capacity(rectangle_count); + + for _ in 0..rectangle_count { + rectangles.push(BitmapData::decode(src)?); + } + + Ok(Self { rectangles }) + } +} + +/// TS_BITMAP_DATA +#[derive(Clone, PartialEq, Eq)] +pub struct BitmapData<'a> { + pub rectangle: InclusiveRectangle, + pub width: u16, + pub height: u16, + pub bits_per_pixel: u16, + pub compression_flags: Compression, + pub compressed_data_header: Option, + pub bitmap_data: &'a [u8], +} + +impl BitmapData<'_> { + const NAME: &'static str = "TS_BITMAP_DATA"; + const FIXED_PART_SIZE: usize = InclusiveRectangle::ENCODED_SIZE + 2 /* width */ + 2 /* height */ + 2 /* bpp */ + 2 /* flags */ + 2 /* len */; + + fn encoded_bitmap_data_length(&self) -> usize { + self.bitmap_data.len() + self.compressed_data_header.as_ref().map(|hdr| hdr.size()).unwrap_or(0) + } +} + +impl Encode for BitmapData<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let encoded_bitmap_data_length = self.encoded_bitmap_data_length(); + let encoded_bitmap_data_length = cast_length!("bitmap data length", encoded_bitmap_data_length)?; + + self.rectangle.encode(dst)?; + dst.write_u16(self.width); + dst.write_u16(self.height); + dst.write_u16(self.bits_per_pixel); + dst.write_u16(self.compression_flags.bits()); + dst.write_u16(encoded_bitmap_data_length); + if let Some(compressed_data_header) = &self.compressed_data_header { + compressed_data_header.encode(dst)?; + }; + dst.write_slice(self.bitmap_data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.encoded_bitmap_data_length() + } +} + +impl<'de> Decode<'de> for BitmapData<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let rectangle = InclusiveRectangle::decode(src)?; + let width = src.read_u16(); + let height = src.read_u16(); + let bits_per_pixel = src.read_u16(); + let compression_flags = Compression::from_bits_truncate(src.read_u16()); + + // A 16-bit, unsigned integer. The size in bytes of the data in the bitmapComprHdr + // and bitmapDataStream fields. + let encoded_bitmap_data_length = usize::from(src.read_u16()); + + ensure_size!(in: src, size: encoded_bitmap_data_length); + + let (compressed_data_header, buffer_length) = if compression_flags.contains(Compression::BITMAP_COMPRESSION) + && !compression_flags.contains(Compression::NO_BITMAP_COMPRESSION_HDR) + { + // Check if encoded_bitmap_data_length is at least CompressedDataHeader::ENCODED_SIZE + if encoded_bitmap_data_length < CompressedDataHeader::ENCODED_SIZE { + return Err(invalid_field_err!( + "cbCompEncodedBitmapDataLength", + "length is less than CompressedDataHeader::ENCODED_SIZE" + )); + } + + let buffer_length = encoded_bitmap_data_length - CompressedDataHeader::ENCODED_SIZE; + (Some(CompressedDataHeader::decode(src)?), buffer_length) + } else { + (None, encoded_bitmap_data_length) + }; + + let bitmap_data = src.read_slice(buffer_length); + + Ok(BitmapData { + rectangle, + width, + height, + bits_per_pixel, + compression_flags, + compressed_data_header, + bitmap_data, + }) + } +} + +impl Debug for BitmapData<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BitmapData") + .field("rectangle", &self.rectangle) + .field("width", &self.width) + .field("height", &self.height) + .field("bits_per_pixel", &self.bits_per_pixel) + .field("compression_flags", &self.compression_flags) + .field("compressed_data_header", &self.compressed_data_header) + .field("bitmap_data.len()", &self.bitmap_data.len()) + .finish() + } +} + +/// TS_CD_HEADER +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompressedDataHeader { + pub main_body_size: u16, + pub scan_width: u16, + pub uncompressed_size: u16, +} + +impl CompressedDataHeader { + const NAME: &'static str = "TS_CD_HEADER"; + const FIXED_PART_SIZE: usize = 2 /* row_size */ + 2 /* body_size */ + 2 /* scan_width */ + 2 /* uncompressed_size */; + + pub const ENCODED_SIZE: usize = Self::FIXED_PART_SIZE; +} + +impl<'de> Decode<'de> for CompressedDataHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let size = src.read_u16(); + if size != FIRST_ROW_SIZE_VALUE { + return Err(invalid_field_err!("cbCompFirstRowSize", "invalid first row size")); + } + + let main_body_size = src.read_u16(); + let scan_width = src.read_u16(); + + if scan_width % 4 != 0 { + return Err(invalid_field_err!( + "cbScanWidth", + "The width of the bitmap must be divisible by 4" + )); + } + let uncompressed_size = src.read_u16(); + + Ok(Self { + main_body_size, + scan_width, + uncompressed_size, + }) + } +} + +impl Encode for CompressedDataHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + if self.scan_width % 4 != 0 { + return Err(invalid_field_err!( + "cbScanWidth", + "The width of the bitmap must be divisible by 4" + )); + } + dst.write_u16(FIRST_ROW_SIZE_VALUE); + dst.write_u16(self.main_body_size); + dst.write_u16(self.scan_width); + dst.write_u16(self.uncompressed_size); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct BitmapFlags: u16{ + const BITMAP_UPDATE_TYPE = 0x0001; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Compression: u16 { + const BITMAP_COMPRESSION = 0x0001; + const NO_BITMAP_COMPRESSION_HDR = 0x0400; + } +} diff --git a/crates/ironrdp-pdu/src/basic_output/bitmap/rdp6.rs b/crates/ironrdp-pdu/src/basic_output/bitmap/rdp6.rs new file mode 100644 index 00000000..bcc1c2e0 --- /dev/null +++ b/crates/ironrdp-pdu/src/basic_output/bitmap/rdp6.rs @@ -0,0 +1,318 @@ +use ironrdp_core::{ + ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +const NON_RLE_PADDING_SIZE: usize = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColorPlaneDefinition { + Argb, + AYCoCg { + color_loss_level: u8, + use_chroma_subsampling: bool, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BitmapStreamHeader { + pub enable_rle_compression: bool, + pub use_alpha: bool, + pub color_plane_definition: ColorPlaneDefinition, +} + +impl BitmapStreamHeader { + pub const NAME: &'static str = "Rdp6BitmapStreamHeader"; + const FIXED_PART_SIZE: usize = 1; +} + +impl Decode<'_> for BitmapStreamHeader { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let header = src.read_u8(); + + let color_loss_level = header & 0x07; + let use_chroma_subsampling = (header & 0x08) != 0; + let enable_rle_compression = (header & 0x10) != 0; + let use_alpha = (header & 0x20) == 0; + + let color_plane_definition = match color_loss_level { + 0 => ColorPlaneDefinition::Argb, + color_loss_level => ColorPlaneDefinition::AYCoCg { + color_loss_level, + use_chroma_subsampling, + }, + }; + + Ok(Self { + enable_rle_compression, + use_alpha, + color_plane_definition, + }) + } +} + +impl Encode for BitmapStreamHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let mut header = (u8::from(self.enable_rle_compression) << 4) | (u8::from(!self.use_alpha) << 5); + + match self.color_plane_definition { + ColorPlaneDefinition::Argb => { + // ARGB color planes keep cll and cs flags set to 0 + } + ColorPlaneDefinition::AYCoCg { + color_loss_level, + use_chroma_subsampling, + .. + } => { + // Add cll and cs flags to header + header |= (color_loss_level & 0x07) | (u8::from(use_chroma_subsampling) << 3); + } + } + + dst.write_u8(header); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + if self.enable_rle_compression { + 0 + } else { + NON_RLE_PADDING_SIZE + } + } +} + +/// Represents `RDP6_BITMAP_STREAM` structure described in [MS-RDPEGDI] 2.2.2.5.1 +#[derive(Debug, Clone)] +pub struct BitmapStream<'a> { + pub header: BitmapStreamHeader, + pub color_planes: &'a [u8], +} + +impl<'a> BitmapStream<'a> { + pub const NAME: &'static str = "Rdp6BitmapStream"; + const FIXED_PART_SIZE: usize = 1; + + pub fn color_panes_data(&self) -> &'a [u8] { + self.color_planes + } + + pub fn has_subsampled_chroma(&self) -> bool { + match self.header.color_plane_definition { + ColorPlaneDefinition::Argb => false, + ColorPlaneDefinition::AYCoCg { + use_chroma_subsampling, .. + } => use_chroma_subsampling, + } + } +} + +impl<'a> Decode<'a> for BitmapStream<'a> { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let header = ironrdp_core::decode_cursor::(src)?; + + let color_planes_size = if !header.enable_rle_compression { + // Cut padding field if RLE flags is set to 0 + if src.is_empty() { + return Err(invalid_field_err!( + "padding", + "missing padding byte from zero-sized non-RLE bitmap data", + )); + } + src.len() - NON_RLE_PADDING_SIZE + } else { + src.len() + }; + + let color_planes = src.read_slice(color_planes_size); + + Ok(Self { header, color_planes }) + } +} + +impl Encode for BitmapStream<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + ironrdp_core::encode_cursor(&self.header, dst)?; + dst.write_slice(self.color_panes_data()); + + // Write padding + if !self.header.enable_rle_compression { + dst.write_u8(0); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.header.size() + self.color_panes_data().len() + } +} + +#[cfg(test)] +#[cfg(feature = "alloc")] +#[expect( + clippy::needless_raw_strings, + reason = "the lint is disable to not interfere with expect! macro" +)] +mod tests { + use expect_test::{expect, Expect}; + + use super::*; + + fn assert_roundtrip(buffer: &[u8], expected: Expect) { + let pdu = ironrdp_core::decode::>(buffer).unwrap(); + expected.assert_debug_eq(&pdu); + assert_eq!(pdu.size(), buffer.len()); + let reencoded = ironrdp_core::encode_vec(&pdu).unwrap(); + assert_eq!(reencoded.as_slice(), buffer); + } + + fn assert_parsing_failure(buffer: &[u8], expected: Expect) { + let error = ironrdp_core::decode::>(buffer).err().unwrap(); + expected.assert_debug_eq(&error); + } + + #[test] + fn parsing_valid_data_succeeds() { + // AYCoCg color planes, with RLE + assert_roundtrip( + &[0x3F, 0x01, 0x02, 0x03, 0x04], + expect![[r#" + BitmapStream { + header: BitmapStreamHeader { + enable_rle_compression: true, + use_alpha: false, + color_plane_definition: AYCoCg { + color_loss_level: 7, + use_chroma_subsampling: true, + }, + }, + color_planes: [ + 1, + 2, + 3, + 4, + ], + } + "#]], + ); + + // RGB color planes, with RLE, with alpha + assert_roundtrip( + &[0x10, 0x01, 0x02, 0x03, 0x04], + expect![[r#" + BitmapStream { + header: BitmapStreamHeader { + enable_rle_compression: true, + use_alpha: true, + color_plane_definition: Argb, + }, + color_planes: [ + 1, + 2, + 3, + 4, + ], + } + "#]], + ); + + // Without RLE, validate that padding is handled correctly + assert_roundtrip( + &[0x20, 0x01, 0x02, 0x03, 0x00], + expect![[r#" + BitmapStream { + header: BitmapStreamHeader { + enable_rle_compression: false, + use_alpha: false, + color_plane_definition: Argb, + }, + color_planes: [ + 1, + 2, + 3, + ], + } + "#]], + ); + + // Empty color planes, with RLE + assert_roundtrip( + &[0x10], + expect![[r#" + BitmapStream { + header: BitmapStreamHeader { + enable_rle_compression: true, + use_alpha: true, + color_plane_definition: Argb, + }, + color_planes: [], + } + "#]], + ); + + // Empty color planes, without RLE + assert_roundtrip( + &[0x00, 0x00], + expect![[r#" + BitmapStream { + header: BitmapStreamHeader { + enable_rle_compression: false, + use_alpha: true, + color_plane_definition: Argb, + }, + color_planes: [], + } + "#]], + ); + } + + #[test] + fn failures_handled_gracefully() { + // Empty buffer + assert_parsing_failure( + &[], + expect![[r#" + Error { + context: "::decode", + kind: NotEnoughBytes { + received: 0, + expected: 1, + }, + source: None, + } + "#]], + ); + + // Without RLE, Check that missing padding byte is handled correctly + assert_parsing_failure( + &[0x20], + expect![[r#" + Error { + context: "::decode", + kind: InvalidField { + field: "padding", + reason: "missing padding byte from zero-sized non-RLE bitmap data", + }, + source: None, + } + "#]], + ); + } +} diff --git a/crates/ironrdp-pdu/src/basic_output/bitmap/tests.rs b/crates/ironrdp-pdu/src/basic_output/bitmap/tests.rs new file mode 100644 index 00000000..db490f4b --- /dev/null +++ b/crates/ironrdp-pdu/src/basic_output/bitmap/tests.rs @@ -0,0 +1,78 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode}; + +use super::*; + +const BITMAP_BUFFER: [u8; 114] = [ + 0x01, 0x00, // Bitmap update type = must be PDATETYPE_BITMAP (0x0001) + 0x01, 0x00, // Number of rectangles = 1 + // Rectangle + 0x00, 0x07, // Left bound of the rectangle = 1792 + 0x00, 0x04, // Top bound of the rectangle = 1024 + 0x3f, 0x07, // Right bound of the rectangle = 1855 + 0x37, 0x04, // Bottom bound of the rectangle = 1079 + 0x40, 0x00, // The width of the rectangle = 64 + 0x38, 0x00, // The height of the rectangle = 56 + 0x10, 0x00, // The color depth of the rectangle data in bits-per-pixel = 16 + 0x01, + // The flag which describes the format of the bitmap data: + // BITMAP_COMPRESSION | !NO_BITMAP_COMPRESSION_HDR => bitmapComprHdr is present + 0x00, // The size in bytes of the data in CompressedDataHeader and bitmap_data = 92 + 0x5c, 0x00, // CompressedDataHeader + 0x00, 0x00, // FirstRowSize, must be set to 0x0000 = 0 + 0x50, 0x00, // MainBodySize - size in bytes of the compressed bitmap data = 80 + 0x1c, 0x00, // ScanWidth - width of the bitmap in pixels(must be divisible by 4) = 28 + // UncompressedSize - size in bytes of the bitmap data after it has been decompressed = 4 + 0x04, 0x00, // Bitmap data + 0x21, 0x00, 0x21, 0x00, 0x01, 0x00, 0x20, 0x09, 0x84, 0x21, 0x00, 0x21, 0x00, 0x21, 0x00, 0x12, 0x00, 0x10, 0xd8, + 0x20, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x1e, 0x21, 0x00, 0x00, 0xa8, 0x83, 0x21, 0x00, + 0x55, 0xad, 0xff, 0xff, 0x45, 0x29, 0x7a, 0xce, 0xa3, 0x10, 0x0e, 0x82, 0x45, 0x29, 0x7a, 0xce, 0xd5, 0x82, 0x10, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0e, 0x45, 0x29, 0x9e, 0xf7, 0xff, 0xff, 0x9e, 0xf7, 0x45, 0x29, 0x21, 0x00, + 0x55, 0xad, 0x10, 0x10, 0xa8, 0xd8, 0x60, 0x12, +]; + +static BITMAP: LazyLock> = LazyLock::new(|| BitmapUpdateData { + rectangles: { + let vec = vec![BitmapData { + rectangle: InclusiveRectangle { + left: 1792, + top: 1024, + right: 1855, + bottom: 1079, + }, + width: 64, + height: 56, + bits_per_pixel: 16, + compression_flags: Compression::BITMAP_COMPRESSION, + compressed_data_header: Some(CompressedDataHeader { + main_body_size: 80, + scan_width: 28, + uncompressed_size: 4, + }), + bitmap_data: &BITMAP_BUFFER[30..], + }]; + vec + }, +}); + +#[test] +fn from_buffer_bitmap_data_parsses_correctly() { + let actual = decode::>(BITMAP_BUFFER.as_ref()).unwrap(); + assert_eq!(*BITMAP, actual); +} + +#[test] +fn to_buffer_bitmap_data_serializes_correctly() { + let expected = BITMAP_BUFFER.as_ref(); + let mut buffer = vec![0; expected.len()]; + encode(&*BITMAP, buffer.as_mut_slice()).unwrap(); + assert_eq!(expected, buffer.as_slice()); +} + +#[test] +fn bitmap_data_length_is_correct() { + let actual = decode::>(BITMAP_BUFFER.as_ref()).unwrap(); + let actual = actual.rectangles.first().unwrap().bitmap_data.len(); + assert_eq!(BITMAP_BUFFER[30..].len(), actual) +} diff --git a/crates/ironrdp-pdu/src/basic_output/fast_path/mod.rs b/crates/ironrdp-pdu/src/basic_output/fast_path/mod.rs new file mode 100644 index 00000000..c0c11aac --- /dev/null +++ b/crates/ironrdp-pdu/src/basic_output/fast_path/mod.rs @@ -0,0 +1,393 @@ +#[cfg(test)] +mod tests; + +use bit_field::BitField as _; +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, decode_cursor, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeError, + DecodeResult, Encode, EncodeResult, InvalidFieldErr as _, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use super::bitmap::BitmapUpdateData; +use super::pointer::PointerUpdateData; +use super::surface_commands::{SurfaceCommand, SURFACE_COMMAND_HEADER_SIZE}; +use crate::per; +use crate::rdp::client_info::CompressionType; +use crate::rdp::headers::{CompressionFlags, SHARE_DATA_HEADER_COMPRESSION_MASK}; + +/// Implements the Fast-Path RDP message header PDU. +/// TS_FP_UPDATE_PDU +#[expect( + clippy::partial_pub_fields, + reason = "this structure is used in the match expression in the integration tests" +)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FastPathHeader { + pub flags: EncryptionFlags, + pub data_length: usize, + forced_long_length: bool, +} + +impl FastPathHeader { + const NAME: &'static str = "TS_FP_UPDATE_PDU header"; + const FIXED_PART_SIZE: usize = 1 /* EncryptionFlags */; + + pub fn new(flags: EncryptionFlags, data_length: usize) -> Self { + Self { + flags, + data_length, + forced_long_length: false, + } + } + + fn minimal_size(&self) -> usize { + // it may then be +2 if > 0x7f + let len = self.data_length + Self::FIXED_PART_SIZE + 1; + + Self::FIXED_PART_SIZE + per::sizeof_length(len) + } +} + +impl Encode for FastPathHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let mut header = 0u8; + header.set_bits(0..2, 0); // fast-path action + header.set_bits(6..8, self.flags.bits()); + dst.write_u8(header); + + let length = self.data_length + self.size(); + let length = cast_length!("length", length)?; + + if self.forced_long_length { + // Preserve same layout for header as received + per::write_long_length(dst, length); + } else { + per::write_length(dst, length); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + if self.forced_long_length { + Self::FIXED_PART_SIZE + per::U16_SIZE + } else { + self.minimal_size() + } + } +} + +impl<'de> Decode<'de> for FastPathHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let header = src.read_u8(); + let flags = EncryptionFlags::from_bits_truncate(header.get_bits(6..8)); + + let (length, sizeof_length) = per::read_length(src).map_err(|e| { + DecodeError::invalid_field("", "length", "Invalid encoded fast path PDU length").with_source(e) + })?; + let length = usize::from(length); + if length < sizeof_length + Self::FIXED_PART_SIZE { + return Err(invalid_field_err!( + "length", + "received fastpath PDU length is smaller than header size" + )); + } + let data_length = length - sizeof_length - Self::FIXED_PART_SIZE; + // Detect case, when received packet has non-optimal packet length packing. + let forced_long_length = per::sizeof_length(length) != sizeof_length; + + Ok(FastPathHeader { + flags, + data_length, + forced_long_length, + }) + } +} + +/// TS_FP_UPDATE +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FastPathUpdatePdu<'a> { + pub fragmentation: Fragmentation, + pub update_code: UpdateCode, + pub compression_flags: Option, + // NOTE: always Some when compression flags is Some + pub compression_type: Option, + pub data: &'a [u8], +} + +impl FastPathUpdatePdu<'_> { + const NAME: &'static str = "TS_FP_UPDATE"; + const FIXED_PART_SIZE: usize = 1 /* header */; +} + +impl Encode for FastPathUpdatePdu<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let data_len = cast_length!("data length", self.data.len())?; + + let mut header = 0u8; + header.set_bits(0..4, self.update_code.as_u8()); + header.set_bits(4..6, self.fragmentation.as_u8()); + + dst.write_u8(header); + + if self.compression_flags.is_some() { + header.set_bits(6..8, Compression::COMPRESSION_USED.bits()); + let compression_flags_with_type = + self.compression_flags.map(|f| f.bits()).unwrap_or(0) | self.compression_type.map_or(0, |f| f.as_u8()); + dst.write_u8(compression_flags_with_type); + } + + dst.write_u16(data_len); + dst.write_slice(self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let compression_flags_size = if self.compression_flags.is_some() { 1 } else { 0 }; + + Self::FIXED_PART_SIZE + compression_flags_size + 2 /* len */ + self.data.len() + } +} + +impl<'de> Decode<'de> for FastPathUpdatePdu<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let header = src.read_u8(); + + let update_code = header.get_bits(0..4); + let update_code = UpdateCode::from_u8(update_code) + .ok_or_else(|| invalid_field_err!("updateHeader", "Invalid update code"))?; + + let fragmentation = header.get_bits(4..6); + let fragmentation = Fragmentation::from_u8(fragmentation) + .ok_or_else(|| invalid_field_err!("updateHeader", "Invalid fragmentation"))?; + + let compression = Compression::from_bits_truncate(header.get_bits(6..8)); + + let (compression_flags, compression_type) = if compression.contains(Compression::COMPRESSION_USED) { + let expected_size = 1 /* flags_with_type */ + 2 /* len */; + ensure_size!(in: src, size: expected_size); + + let compression_flags_with_type = src.read_u8(); + let compression_flags = + CompressionFlags::from_bits_truncate(compression_flags_with_type & !SHARE_DATA_HEADER_COMPRESSION_MASK); + let compression_type = + CompressionType::from_u8(compression_flags_with_type & SHARE_DATA_HEADER_COMPRESSION_MASK) + .ok_or_else(|| invalid_field_err!("compressionFlags", "invalid compression type"))?; + + (Some(compression_flags), Some(compression_type)) + } else { + let expected_size = 2 /* len */; + ensure_size!(in: src, size: expected_size); + + (None, None) + }; + + let data_length = usize::from(src.read_u16()); + ensure_size!(in: src, size: data_length); + let data = src.read_slice(data_length); + + Ok(Self { + fragmentation, + update_code, + compression_flags, + compression_type, + data, + }) + } +} + +/// TS_FP_UPDATE data +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FastPathUpdate<'a> { + SurfaceCommands(Vec>), + Bitmap(BitmapUpdateData<'a>), + Pointer(PointerUpdateData<'a>), +} + +impl<'a> FastPathUpdate<'a> { + const NAME: &'static str = "TS_FP_UPDATE data"; + + pub fn decode_with_code(src: &'a [u8], code: UpdateCode) -> DecodeResult { + let mut cursor = ReadCursor::<'a>::new(src); + Self::decode_cursor_with_code(&mut cursor, code) + } + + pub fn decode_cursor_with_code(src: &mut ReadCursor<'a>, code: UpdateCode) -> DecodeResult { + match code { + UpdateCode::SurfaceCommands => { + let mut commands = Vec::with_capacity(1); + while src.len() >= SURFACE_COMMAND_HEADER_SIZE { + commands.push(decode_cursor::>(src)?); + } + + Ok(Self::SurfaceCommands(commands)) + } + UpdateCode::Bitmap => Ok(Self::Bitmap(decode_cursor(src)?)), + UpdateCode::HiddenPointer => Ok(Self::Pointer(PointerUpdateData::SetHidden)), + UpdateCode::DefaultPointer => Ok(Self::Pointer(PointerUpdateData::SetDefault)), + UpdateCode::PositionPointer => Ok(Self::Pointer(PointerUpdateData::SetPosition(decode_cursor(src)?))), + UpdateCode::ColorPointer => { + let color = decode_cursor(src)?; + Ok(Self::Pointer(PointerUpdateData::Color(color))) + } + UpdateCode::CachedPointer => Ok(Self::Pointer(PointerUpdateData::Cached(decode_cursor(src)?))), + UpdateCode::NewPointer => Ok(Self::Pointer(PointerUpdateData::New(decode_cursor(src)?))), + UpdateCode::LargePointer => Ok(Self::Pointer(PointerUpdateData::Large(decode_cursor(src)?))), + _ => Err(invalid_field_err!("updateCode", "unsupported fast-path update code")), + } + } + + pub fn as_short_name(&self) -> &str { + match self { + Self::SurfaceCommands(_) => "Surface Commands", + Self::Bitmap(_) => "Bitmap", + Self::Pointer(_) => "Pointer", + } + } +} + +impl Encode for FastPathUpdate<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + match self { + Self::SurfaceCommands(commands) => { + for command in commands { + command.encode(dst)?; + } + } + Self::Bitmap(bitmap) => { + bitmap.encode(dst)?; + } + Self::Pointer(pointer) => match pointer { + PointerUpdateData::SetHidden => {} + PointerUpdateData::SetDefault => {} + PointerUpdateData::SetPosition(inner) => inner.encode(dst)?, + PointerUpdateData::Color(inner) => inner.encode(dst)?, + PointerUpdateData::Cached(inner) => inner.encode(dst)?, + PointerUpdateData::New(inner) => inner.encode(dst)?, + PointerUpdateData::Large(inner) => inner.encode(dst)?, + }, + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + match self { + Self::SurfaceCommands(commands) => commands.iter().map(|c| c.size()).sum::(), + Self::Bitmap(bitmap) => bitmap.size(), + Self::Pointer(pointer) => match pointer { + PointerUpdateData::SetHidden => 0, + PointerUpdateData::SetDefault => 0, + PointerUpdateData::SetPosition(inner) => inner.size(), + PointerUpdateData::Color(inner) => inner.size(), + PointerUpdateData::Cached(inner) => inner.size(), + PointerUpdateData::New(inner) => inner.size(), + PointerUpdateData::Large(inner) => inner.size(), + }, + } + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum UpdateCode { + Orders = 0x0, + Bitmap = 0x1, + Palette = 0x2, + Synchronize = 0x3, + SurfaceCommands = 0x4, + HiddenPointer = 0x5, + DefaultPointer = 0x6, + PositionPointer = 0x8, + ColorPointer = 0x9, + CachedPointer = 0xa, + NewPointer = 0xb, + LargePointer = 0xc, +} + +impl UpdateCode { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +impl From<&FastPathUpdate<'_>> for UpdateCode { + fn from(update: &FastPathUpdate<'_>) -> Self { + match update { + FastPathUpdate::SurfaceCommands(_) => Self::SurfaceCommands, + FastPathUpdate::Bitmap(_) => Self::Bitmap, + FastPathUpdate::Pointer(action) => match action { + PointerUpdateData::SetHidden => Self::HiddenPointer, + PointerUpdateData::SetDefault => Self::DefaultPointer, + PointerUpdateData::SetPosition(_) => Self::PositionPointer, + PointerUpdateData::Color(_) => Self::ColorPointer, + PointerUpdateData::Cached(_) => Self::CachedPointer, + PointerUpdateData::New(_) => Self::NewPointer, + PointerUpdateData::Large(_) => Self::LargePointer, + }, + } + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum Fragmentation { + Single = 0x0, + Last = 0x1, + First = 0x2, + Next = 0x3, +} + +impl Fragmentation { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct EncryptionFlags: u8 { + const SECURE_CHECKSUM = 0x1; + const ENCRYPTED = 0x2; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Compression: u8 { + const COMPRESSION_USED = 0x2; + } +} diff --git a/crates/ironrdp-pdu/src/basic_output/fast_path/tests.rs b/crates/ironrdp-pdu/src/basic_output/fast_path/tests.rs new file mode 100644 index 00000000..04c84d2d --- /dev/null +++ b/crates/ironrdp-pdu/src/basic_output/fast_path/tests.rs @@ -0,0 +1,157 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode}; + +use super::*; + +const FAST_PATH_HEADER_WITH_SHORT_LEN_BUFFER: [u8; 2] = [0x80, 0x08]; +const FAST_PATH_HEADER_WITH_LONG_LEN_BUFFER: [u8; 3] = [0x80, 0x81, 0xE7]; +const FAST_PATH_UPDATE_PDU_BUFFER: [u8; 19] = [ + 0x4, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, +]; +const FAST_PATH_UPDATE_PDU_WITH_LONG_LEN_BUFFER: [u8; 19] = [ + 0x4, 0xff, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, +]; +const FAST_PATH_HEADER_WITH_FORCED_LONG_LEN_BUFFER: [u8; 3] = [0x80, 0x80, 0x08]; + +const FAST_PATH_HEADER_WITH_SHORT_LEN_PDU: FastPathHeader = FastPathHeader { + flags: EncryptionFlags::ENCRYPTED, + data_length: 6, + forced_long_length: false, +}; +const FAST_PATH_HEADER_WITH_LONG_LEN_PDU: FastPathHeader = FastPathHeader { + flags: EncryptionFlags::ENCRYPTED, + data_length: 484, + forced_long_length: false, +}; +const FAST_PATH_HEADER_WITH_FORCED_LONG_LEN_PDU: FastPathHeader = FastPathHeader { + flags: EncryptionFlags::ENCRYPTED, + data_length: 5, + forced_long_length: true, +}; + +static FAST_PATH_UPDATE_PDU: LazyLock> = LazyLock::new(|| FastPathUpdatePdu { + fragmentation: Fragmentation::Single, + update_code: UpdateCode::SurfaceCommands, + compression_flags: None, + compression_type: None, + data: &FAST_PATH_UPDATE_PDU_BUFFER[3..], +}); + +#[test] +fn from_buffer_correctly_parses_fast_path_header_with_short_length() { + assert_eq!( + FAST_PATH_HEADER_WITH_SHORT_LEN_PDU, + decode::(FAST_PATH_HEADER_WITH_SHORT_LEN_BUFFER.as_ref()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_fast_path_header_with_short_length() { + let expected = FAST_PATH_HEADER_WITH_SHORT_LEN_BUFFER.as_ref(); + let mut buffer = vec![0; expected.len()]; + + encode(&FAST_PATH_HEADER_WITH_SHORT_LEN_PDU, buffer.as_mut_slice()).unwrap(); + assert_eq!(expected, buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_fast_path_header_with_short_length() { + assert_eq!( + FAST_PATH_HEADER_WITH_SHORT_LEN_BUFFER.len(), + FAST_PATH_HEADER_WITH_SHORT_LEN_PDU.size() + ); +} + +#[test] +fn from_buffer_correctly_parses_fast_path_header_with_long_length() { + assert_eq!( + FAST_PATH_HEADER_WITH_LONG_LEN_PDU, + decode::(FAST_PATH_HEADER_WITH_LONG_LEN_BUFFER.as_ref()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_fast_path_header_with_long_length() { + let expected = FAST_PATH_HEADER_WITH_LONG_LEN_BUFFER.as_ref(); + let mut buffer = vec![0; expected.len()]; + + encode(&FAST_PATH_HEADER_WITH_LONG_LEN_PDU, buffer.as_mut_slice()).unwrap(); + assert_eq!(expected, buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_fast_path_header_with_long_length() { + assert_eq!( + FAST_PATH_HEADER_WITH_LONG_LEN_BUFFER.len(), + FAST_PATH_HEADER_WITH_LONG_LEN_PDU.size() + ); +} + +#[test] +fn from_buffer_correctly_parses_fast_path_header_with_forced_long_length() { + assert_eq!( + FAST_PATH_HEADER_WITH_FORCED_LONG_LEN_PDU, + decode::(FAST_PATH_HEADER_WITH_FORCED_LONG_LEN_BUFFER.as_ref()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_fast_path_header_with_forced_long_length() { + let expected = FAST_PATH_HEADER_WITH_FORCED_LONG_LEN_BUFFER.as_ref(); + let mut buffer = vec![0; expected.len()]; + + encode(&FAST_PATH_HEADER_WITH_FORCED_LONG_LEN_PDU, buffer.as_mut_slice()).unwrap(); + assert_eq!(expected, buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_fast_path_header_with_forced_long_length() { + assert_eq!( + FAST_PATH_HEADER_WITH_FORCED_LONG_LEN_BUFFER.len(), + FAST_PATH_HEADER_WITH_FORCED_LONG_LEN_PDU.size() + ); +} + +#[test] +fn from_buffer_correctly_parses_fast_path_update() { + assert_eq!( + *FAST_PATH_UPDATE_PDU, + decode::>(FAST_PATH_UPDATE_PDU_BUFFER.as_ref()).unwrap() + ); +} + +#[test] +fn from_buffer_returns_error_on_long_length_for_fast_path_update() { + assert!(decode::>(FAST_PATH_UPDATE_PDU_WITH_LONG_LEN_BUFFER.as_ref()).is_err()); +} + +#[test] +fn to_buffer_correctly_serializes_fast_path_update() { + let expected = FAST_PATH_UPDATE_PDU_BUFFER.as_ref(); + let mut buffer = vec![0; expected.len()]; + + encode(&*FAST_PATH_UPDATE_PDU, buffer.as_mut_slice()).unwrap(); + assert_eq!(expected, buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_fast_path_update() { + assert_eq!(FAST_PATH_UPDATE_PDU_BUFFER.len(), FAST_PATH_UPDATE_PDU.size()); +} + +#[test] +fn buffer_size_boundary_fast_path_update() { + let fph = FastPathHeader { + flags: EncryptionFlags::ENCRYPTED, + data_length: 125, + forced_long_length: false, + }; + assert_eq!(fph.size(), 2); + let fph = FastPathHeader { + flags: EncryptionFlags::ENCRYPTED, + data_length: 126, + forced_long_length: false, + }; + assert_eq!(fph.size(), 3); +} diff --git a/crates/ironrdp-pdu/src/basic_output/mod.rs b/crates/ironrdp-pdu/src/basic_output/mod.rs new file mode 100644 index 00000000..3e287b77 --- /dev/null +++ b/crates/ironrdp-pdu/src/basic_output/mod.rs @@ -0,0 +1,4 @@ +pub mod bitmap; +pub mod fast_path; +pub mod pointer; +pub mod surface_commands; diff --git a/crates/ironrdp-pdu/src/basic_output/pointer/mod.rs b/crates/ironrdp-pdu/src/basic_output/pointer/mod.rs new file mode 100644 index 00000000..04bd62c6 --- /dev/null +++ b/crates/ironrdp-pdu/src/basic_output/pointer/mod.rs @@ -0,0 +1,337 @@ +use ironrdp_core::{ + cast_int, cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, + EncodeResult, ReadCursor, WriteCursor, +}; + +// Represents `TS_POINT16` described in [MS-RDPBCGR] 2.2.9.1.1.4.1 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Point16 { + pub x: u16, + pub y: u16, +} + +impl Point16 { + const NAME: &'static str = "TS_POINT16"; + const FIXED_PART_SIZE: usize = 2 /* x */ + 2 /* y */; +} + +impl Encode for Point16 { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.x); + dst.write_u16(self.y); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for Point16 { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let x = src.read_u16(); + let y = src.read_u16(); + + Ok(Self { x, y }) + } +} + +/// According to [MS-RDPBCGR] 2.2.9.1.1.4.2 `TS_POINTERPOSATTRIBUTE` has the same layout +/// as `TS_POINT16` +pub type PointerPositionAttribute = Point16; + +/// Represents `TS_COLORPOINTERATTRIBUTE` described in [MS-RDPBCGR] 2.2.9.1.1.4.4 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ColorPointerAttribute<'a> { + pub cache_index: u16, + pub hot_spot: Point16, + pub width: u16, + pub height: u16, + pub xor_mask: &'a [u8], + pub and_mask: &'a [u8], +} + +impl ColorPointerAttribute<'_> { + const NAME: &'static str = "TS_COLORPOINTERATTRIBUTE"; + const FIXED_PART_SIZE: usize = + 2 /* cacheIdx */ + 2 /* width */ + 2 /* height */ + 2 /* lenAnd */ + 2 /* lenOr */ + Point16::FIXED_PART_SIZE; +} + +macro_rules! check_masks_alignment { + ($and_mask:expr, $xor_mask:expr, $pointer_height:expr, $large_ptr:expr) => {{ + const AND_MASK_SIZE_FIELD: &str = "lengthAndMask"; + const XOR_MASK_SIZE_FIELD: &str = "lengthXorMask"; + const U32_MAX: usize = 0xFFFFFFFF; + + let pointer_height: usize = cast_int!("pointer height", $pointer_height)?; + + let check_mask = |mask: &[u8], field: &'static str| { + if $pointer_height == 0 { + return Err(invalid_field_err!(field, "pointer height cannot be zero")); + } + if $large_ptr && (mask.len() > U32_MAX) { + return Err(invalid_field_err!(field, "pointer mask is too big for u32 size")); + } + if !$large_ptr && (mask.len() > usize::from(u16::MAX)) { + return Err(invalid_field_err!(field, "pointer mask is too big for u16 size")); + } + if (mask.len() % pointer_height) != 0 { + return Err(invalid_field_err!(field, "pointer mask have incomplete scanlines")); + } + if (mask.len() / pointer_height) % 2 != 0 { + return Err(invalid_field_err!( + field, + "pointer mask scanlines should be aligned to 16 bits" + )); + } + Ok(()) + }; + + check_mask($and_mask, AND_MASK_SIZE_FIELD)?; + check_mask($xor_mask, XOR_MASK_SIZE_FIELD) + }}; +} + +impl Encode for ColorPointerAttribute<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + check_masks_alignment!(self.and_mask, self.xor_mask, self.height, false)?; + + dst.write_u16(self.cache_index); + self.hot_spot.encode(dst)?; + dst.write_u16(self.width); + dst.write_u16(self.height); + + dst.write_u16(cast_length!("and mask length", self.and_mask.len())?); + dst.write_u16(cast_length!("xor mask length", self.xor_mask.len())?); + // Note that masks are written in reverse order. It is not a mistake, that is how the + // message is defined in [MS-RDPBCGR] + dst.write_slice(self.xor_mask); + dst.write_slice(self.and_mask); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.xor_mask.len() + self.and_mask.len() + } +} + +impl<'a> Decode<'a> for ColorPointerAttribute<'a> { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let cache_index = src.read_u16(); + let hot_spot = Point16::decode(src)?; + let width = src.read_u16(); + let height = src.read_u16(); + // Convert to usize during the addition to prevent overflow and match expected type + let length_and_mask = usize::from(src.read_u16()); + let length_xor_mask = usize::from(src.read_u16()); + + let expected_masks_size = length_and_mask + length_xor_mask; + ensure_size!(in: src, size: expected_masks_size); + + let xor_mask = src.read_slice(length_xor_mask); + let and_mask = src.read_slice(length_and_mask); + + check_masks_alignment!(and_mask, xor_mask, height, false)?; + + Ok(Self { + cache_index, + hot_spot, + width, + height, + xor_mask, + and_mask, + }) + } +} + +/// Represents `TS_POINTERATTRIBUTE` described in [MS-RDPBCGR] 2.2.9.1.1.4.5 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PointerAttribute<'a> { + pub xor_bpp: u16, + pub color_pointer: ColorPointerAttribute<'a>, +} + +impl PointerAttribute<'_> { + const NAME: &'static str = "TS_POINTERATTRIBUTE"; + const FIXED_PART_SIZE: usize = 2 /* xorBpp */; +} + +impl Encode for PointerAttribute<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.xor_bpp); + self.color_pointer.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.color_pointer.size() + } +} + +impl<'a> Decode<'a> for PointerAttribute<'a> { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let xor_bpp = src.read_u16(); + let color_pointer = ColorPointerAttribute::decode(src)?; + + Ok(Self { xor_bpp, color_pointer }) + } +} + +/// Represents `TS_CACHEDPOINTERATTRIBUTE` described in [MS-RDPBCGR] 2.2.9.1.1.4.6 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CachedPointerAttribute { + pub cache_index: u16, +} + +impl CachedPointerAttribute { + const NAME: &'static str = "TS_CACHEDPOINTERATTRIBUTE"; + const FIXED_PART_SIZE: usize = 2 /* cacheIdx */; +} + +impl Encode for CachedPointerAttribute { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.cache_index); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for CachedPointerAttribute { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let cache_index = src.read_u16(); + + Ok(Self { cache_index }) + } +} + +/// Represents `TS_FP_LARGEPOINTERATTRIBUTE` described in [MS-RDPBCGR] 2.2.9.1.2.1.11 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LargePointerAttribute<'a> { + pub xor_bpp: u16, + pub cache_index: u16, + pub hot_spot: Point16, + pub width: u16, + pub height: u16, + pub xor_mask: &'a [u8], + pub and_mask: &'a [u8], +} + +impl LargePointerAttribute<'_> { + const NAME: &'static str = "TS_FP_LARGEPOINTERATTRIBUTE"; + const FIXED_PART_SIZE: usize = + 2 /* xorBpp */ + 2 /* cacheIdx */ + 4 /* hotSpot */ + 2 /* width */ + 2 /* height */ + + 4 /* andMaskLen */ + 4 /* xorMaskLen */; +} + +impl Encode for LargePointerAttribute<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + check_masks_alignment!(self.and_mask, self.xor_mask, self.height, true)?; + + dst.write_u16(self.xor_bpp); + dst.write_u16(self.cache_index); + self.hot_spot.encode(dst)?; + dst.write_u16(self.width); + dst.write_u16(self.height); + + dst.write_u32(cast_length!("and mask length", self.and_mask.len())?); + dst.write_u32(cast_length!("xor mask length", self.xor_mask.len())?); + // See comment in `ColorPointerAttribute::encode` about encoding order + dst.write_slice(self.xor_mask); + dst.write_slice(self.and_mask); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.xor_mask.len() + self.and_mask.len() + } +} + +impl<'a> Decode<'a> for LargePointerAttribute<'a> { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let xor_bpp = src.read_u16(); + let cache_index = src.read_u16(); + let hot_spot = Point16::decode(src)?; + let width = src.read_u16(); + let height = src.read_u16(); + // Convert to usize to prevent overflow during addition + let length_and_mask = cast_length!("and mask length", src.read_u32())?; + let length_xor_mask = cast_length!("xor mask length", src.read_u32())?; + + let expected_masks_size = length_and_mask + length_xor_mask; + ensure_size!(in: src, size: expected_masks_size); + + let xor_mask = src.read_slice(length_xor_mask); + let and_mask = src.read_slice(length_and_mask); + + check_masks_alignment!(and_mask, xor_mask, height, true)?; + + Ok(Self { + xor_bpp, + cache_index, + hot_spot, + width, + height, + xor_mask, + and_mask, + }) + } +} + +/// Pointer-related FastPath update messages (inner FastPath packet data) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PointerUpdateData<'a> { + SetHidden, + SetDefault, + SetPosition(PointerPositionAttribute), + Color(ColorPointerAttribute<'a>), + Cached(CachedPointerAttribute), + New(PointerAttribute<'a>), + Large(LargePointerAttribute<'a>), +} diff --git a/crates/ironrdp-pdu/src/basic_output/surface_commands/mod.rs b/crates/ironrdp-pdu/src/basic_output/surface_commands/mod.rs new file mode 100644 index 00000000..4f9a6bb4 --- /dev/null +++ b/crates/ironrdp-pdu/src/basic_output/surface_commands/mod.rs @@ -0,0 +1,375 @@ +#[cfg(test)] +mod tests; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use crate::geometry::ExclusiveRectangle; + +pub const SURFACE_COMMAND_HEADER_SIZE: usize = 2; + +// TS_SURFCMD +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SurfaceCommand<'a> { + SetSurfaceBits(SurfaceBitsPdu<'a>), + FrameMarker(FrameMarkerPdu), + StreamSurfaceBits(SurfaceBitsPdu<'a>), +} + +impl SurfaceCommand<'_> { + const NAME: &'static str = "TS_SURFCMD"; + const FIXED_PART_SIZE: usize = 2 /* cmdType */; +} + +impl Encode for SurfaceCommand<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let cmd_type = SurfaceCommandType::from(self); + dst.write_u16(cmd_type.as_u16()); + + match self { + Self::SetSurfaceBits(pdu) | Self::StreamSurfaceBits(pdu) => pdu.encode(dst), + Self::FrameMarker(pdu) => pdu.encode(dst), + }?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + SURFACE_COMMAND_HEADER_SIZE + + match self { + Self::SetSurfaceBits(pdu) | Self::StreamSurfaceBits(pdu) => pdu.size(), + Self::FrameMarker(pdu) => pdu.size(), + } + } +} + +impl<'de> Decode<'de> for SurfaceCommand<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let cmd_type = src.read_u16(); + let cmd_type = SurfaceCommandType::from_u16(cmd_type) + .ok_or_else(|| invalid_field_err!("cmdType", "invalid surface command"))?; + + match cmd_type { + SurfaceCommandType::SetSurfaceBits => Ok(Self::SetSurfaceBits(SurfaceBitsPdu::decode(src)?)), + SurfaceCommandType::FrameMarker => Ok(Self::FrameMarker(FrameMarkerPdu::decode(src)?)), + SurfaceCommandType::StreamSurfaceBits => Ok(Self::StreamSurfaceBits(SurfaceBitsPdu::decode(src)?)), + } + } +} + +// TS_SURFCMD_STREAM_SURF_BITS and TS_SURFCMD_SET_SURF_BITS +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SurfaceBitsPdu<'a> { + pub destination: ExclusiveRectangle, + pub extended_bitmap_data: ExtendedBitmapDataPdu<'a>, +} + +impl SurfaceBitsPdu<'_> { + const NAME: &'static str = "TS_SURFCMD_x_SURFACE_BITS_PDU"; +} + +impl Encode for SurfaceBitsPdu<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + self.destination.encode(dst)?; + self.extended_bitmap_data.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.destination.size() + self.extended_bitmap_data.size() + } +} + +impl<'de> Decode<'de> for SurfaceBitsPdu<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let destination = ExclusiveRectangle::decode(src)?; + let extended_bitmap_data = ExtendedBitmapDataPdu::decode(src)?; + + Ok(Self { + destination, + extended_bitmap_data, + }) + } +} + +// TS_FRAME_MARKER +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FrameMarkerPdu { + pub frame_action: FrameAction, + pub frame_id: Option, +} + +impl FrameMarkerPdu { + const NAME: &'static str = "TS_FRAME_MARKER_PDU"; + const FIXED_PART_SIZE: usize = 2 /* frameAction */ + 4 /* frameId */; +} + +impl Encode for FrameMarkerPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.frame_action.as_u16()); + dst.write_u32(self.frame_id.unwrap_or(0)); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for FrameMarkerPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_size!(in: src, size: 2); + + let frame_action = src.read_u16(); + + let frame_action = FrameAction::from_u16(frame_action) + .ok_or_else(|| invalid_field_err!("frameAction", "invalid frame action"))?; + + let frame_id = if src.is_empty() { + // Sometimes Windows 10 RDP server sends not complete FrameMarker PDU (without frame ID), + // so we made frame ID field as optional (not officially) + + None + } else { + ensure_size!(in: src, size: 4); + Some(src.read_u32()) + }; + + Ok(Self { frame_action, frame_id }) + } +} + +// TS_BITMAP_DATA_EX +#[derive(Clone, PartialEq, Eq)] +pub struct ExtendedBitmapDataPdu<'a> { + pub bpp: u8, + pub codec_id: u8, + pub width: u16, + pub height: u16, + pub header: Option, + pub data: &'a [u8], +} + +impl core::fmt::Debug for ExtendedBitmapDataPdu<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ExtendedBitmapDataPdu") + .field("bpp", &self.bpp) + .field("codec_id", &self.codec_id) + .field("width", &self.width) + .field("height", &self.height) + .field("header", &self.header) + .field("data_len", &self.data.len()) + .finish() + } +} + +impl ExtendedBitmapDataPdu<'_> { + const NAME: &'static str = "TS_BITMAP_DATA_EX"; + const FIXED_PART_SIZE: usize = 1 /* bpp */ + 1 /* flags */ + 1 /* reserved */ + 1 /* codecId */ + 2 /* width */ + 2 /* height */ + 4 /* len */; +} + +impl Encode for ExtendedBitmapDataPdu<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let data_len = cast_length!("bitmap data length", self.data.len())?; + + dst.write_u8(self.bpp); + let flags = if self.header.is_some() { + BitmapDataFlags::COMPRESSED_BITMAP_HEADER_PRESENT + } else { + BitmapDataFlags::empty() + }; + dst.write_u8(flags.bits()); + dst.write_u8(0); // reserved + dst.write_u8(self.codec_id); + dst.write_u16(self.width); + dst.write_u16(self.height); + dst.write_u32(data_len); + if let Some(header) = &self.header { + header.encode(dst)?; + } + dst.write_slice(self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.header.as_ref().map_or(0, |h| h.size()) + self.data.len() + } +} + +impl<'de> Decode<'de> for ExtendedBitmapDataPdu<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let bpp = src.read_u8(); + let flags = BitmapDataFlags::from_bits_truncate(src.read_u8()); + let _reserved = src.read_u8(); + let codec_id = src.read_u8(); + let width = src.read_u16(); + let height = src.read_u16(); + let data_length = cast_length!("bitmap data length", src.read_u32())?; + + let expected_remaining_size = if flags.contains(BitmapDataFlags::COMPRESSED_BITMAP_HEADER_PRESENT) { + data_length + BitmapDataHeader::ENCODED_SIZE + } else { + data_length + }; + + ensure_size!(in: src, size: expected_remaining_size); + + let header = if flags.contains(BitmapDataFlags::COMPRESSED_BITMAP_HEADER_PRESENT) { + Some(BitmapDataHeader::decode(src)?) + } else { + None + }; + + let data = src.read_slice(data_length); + + Ok(Self { + bpp, + codec_id, + width, + height, + header, + data, + }) + } +} + +// TS_COMPRESSED_BITMAP_HEADER_EX +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BitmapDataHeader { + pub high_unique_id: u32, + pub low_unique_id: u32, + pub tm_milliseconds: u64, + pub tm_seconds: u64, +} + +impl BitmapDataHeader { + const NAME: &'static str = "TS_COMPRESSED_BITMAP_HEADER_EX"; + const FIXED_PART_SIZE: usize = 4 /* highUniqueId */ + 4 /* lowUniqueId */ + 8 /* tmMilli */ + 8 /* tmSeconds */; + + pub const ENCODED_SIZE: usize = Self::FIXED_PART_SIZE; +} + +impl Encode for BitmapDataHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.high_unique_id); + dst.write_u32(self.low_unique_id); + dst.write_u64(self.tm_milliseconds); + dst.write_u64(self.tm_seconds); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for BitmapDataHeader { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let high_unique_id = src.read_u32(); + let low_unique_id = src.read_u32(); + let tm_milliseconds = src.read_u64(); + let tm_seconds = src.read_u64(); + + Ok(Self { + high_unique_id, + low_unique_id, + tm_milliseconds, + tm_seconds, + }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive)] +#[repr(u16)] +enum SurfaceCommandType { + SetSurfaceBits = 0x01, + FrameMarker = 0x04, + StreamSurfaceBits = 0x06, +} + +impl SurfaceCommandType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +impl From<&SurfaceCommand<'_>> for SurfaceCommandType { + fn from(command: &SurfaceCommand<'_>) -> Self { + match command { + SurfaceCommand::SetSurfaceBits(_) => Self::SetSurfaceBits, + SurfaceCommand::FrameMarker(_) => Self::FrameMarker, + SurfaceCommand::StreamSurfaceBits(_) => Self::StreamSurfaceBits, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(u16)] +pub enum FrameAction { + Begin = 0x00, + End = 0x01, +} + +impl FrameAction { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u16(self) -> u16 { + self as u16 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct BitmapDataFlags: u8 { + const COMPRESSED_BITMAP_HEADER_PRESENT = 0x01; + } +} diff --git a/crates/ironrdp-pdu/src/basic_output/surface_commands/tests.rs b/crates/ironrdp-pdu/src/basic_output/surface_commands/tests.rs new file mode 100644 index 00000000..db1db71b --- /dev/null +++ b/crates/ironrdp-pdu/src/basic_output/surface_commands/tests.rs @@ -0,0 +1,140 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode}; + +use super::*; + +const FRAME_MARKER_BUFFER: [u8; 8] = [0x4, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0]; + +const SURFACE_BITS_BUFFER: [u8; 1217] = [ + 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x7, 0x38, 0x4, 0x20, 0x0, 0x0, 0x3, 0x80, 0x7, 0x38, 0x4, 0xab, 0x4, 0x0, 0x0, + 0xc4, 0xcc, 0xe, 0x0, 0x0, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0x1, 0x0, 0xc6, 0xcc, 0x17, 0x0, 0x0, 0x0, 0x1, 0x0, + 0x1, 0x1, 0x0, 0x70, 0x7, 0xa0, 0x0, 0x10, 0x0, 0xc0, 0x0, 0xc1, 0xca, 0x1, 0x0, 0xc7, 0xcc, 0x7e, 0x4, 0x0, 0x0, + 0x1, 0x0, 0xc2, 0xca, 0x0, 0x0, 0x51, 0x50, 0x1, 0x40, 0x4, 0x0, 0x63, 0x4, 0x0, 0x0, 0x66, 0x66, 0x77, 0x88, 0x98, + 0xc3, 0xca, 0x25, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1d, 0x0, 0x2, 0x0, 0x0, 0x1, 0xd, 0x0, 0x5, 0x0, 0x6, 0x41, 0xb8, + 0xbc, 0x5e, 0x7e, 0x2f, 0x3f, 0x17, 0x9f, 0x8b, 0xc3, 0xf9, 0x7c, 0xbe, 0x7e, 0x7e, 0x2f, 0x43, 0xb3, 0x85, 0x68, + 0xe4, 0x70, 0x46, 0x8e, 0x47, 0xa, 0xc8, 0xe4, 0x60, 0x46, 0x47, 0x23, 0x5, 0x64, 0x72, 0x30, 0x23, 0x23, 0x91, + 0x82, 0xb2, 0x39, 0x18, 0x11, 0x91, 0xc8, 0xc1, 0x59, 0x1c, 0x8c, 0x8, 0xc8, 0xe4, 0x60, 0xac, 0x8e, 0x46, 0x4, + 0x64, 0x72, 0x30, 0x56, 0x47, 0x23, 0x2, 0x32, 0x39, 0x18, 0x0, 0xcd, 0xb0, 0x34, 0x1a, 0x1a, 0x1a, 0x34, 0xd6, + 0xb7, 0xe7, 0xc7, 0xc7, 0x4e, 0x9d, 0x3a, 0x69, 0x0, 0x1, 0xf, 0x20, 0xc8, 0x32, 0x19, 0x18, 0x0, 0xf, 0xe6, 0x43, + 0xe4, 0x7c, 0x8f, 0xa7, 0xd7, 0xdf, 0xbf, 0x89, 0x32, 0x8e, 0x82, 0x13, 0xff, 0xe0, 0x84, 0x82, 0x1f, 0xfe, 0x60, + 0x1c, 0xa0, 0x83, 0xff, 0xcc, 0x0, 0xa2, 0x82, 0xf, 0xc6, 0x0, 0x52, 0x82, 0xf, 0xc6, 0x0, 0xa5, 0x4, 0x1f, 0xfe, + 0x60, 0x5, 0x14, 0x10, 0x7e, 0x30, 0x0, 0x18, 0xdc, 0x2e, 0x2e, 0x5c, 0xdb, 0x7f, 0x8f, 0xd3, 0xc9, 0x46, 0x0, + 0x22, 0x10, 0x10, 0xa, 0x13, 0xcb, 0xcb, 0x20, 0x0, 0x7e, 0x11, 0x13, 0xa8, 0x82, 0xd8, 0x8d, 0xc4, 0xc5, 0x88, + 0x4f, 0xf4, 0x9, 0xff, 0xff, 0xd1, 0x6, 0xf8, 0x88, 0x13, 0xe2, 0x20, 0x32, 0x65, 0xaf, 0x1e, 0x38, 0x18, 0x4c, + 0x25, 0x4a, 0xc0, 0x27, 0x80, 0x1a, 0xb, 0xdc, 0x1, 0x47, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbb, 0x0, + 0xa4, 0xd, 0x1, 0x25, 0x80, 0xa8, 0x18, 0x4a, 0x40, 0x18, 0x0, 0x48, 0xa2, 0xa8, 0x0, 0x10, 0x74, 0xd6, 0x80, 0x8, + 0xc4, 0x1b, 0x89, 0x10, 0x0, 0x28, 0xdf, 0xff, 0xf6, 0xa1, 0xc0, 0x0, 0x70, 0xde, 0x4, 0xf2, 0x0, 0x7, 0xd, 0x73, + 0xe4, 0x0, 0x0, 0x0, 0x1f, 0x10, 0x40, 0xb4, 0x4, 0x85, 0xa0, 0x48, 0xb2, 0x40, 0x0, 0x0, 0x0, 0x8, 0x6, 0x0, 0xc3, + 0xca, 0x30, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1d, 0x0, 0x3, 0x0, 0x13, 0x1, 0x5, 0x0, 0x5, 0x0, 0x6, 0x49, 0xc9, + 0x81, 0xf2, 0x39, 0x30, 0x23, 0x23, 0x91, 0x81, 0x19, 0x1c, 0x8c, 0x15, 0x91, 0xc8, 0xc0, 0x8c, 0x8e, 0x46, 0xa, + 0xc8, 0xe4, 0x60, 0x46, 0x47, 0x23, 0x5, 0x64, 0x72, 0x30, 0x23, 0x23, 0x91, 0x82, 0xb2, 0x39, 0x18, 0x11, 0x91, + 0xc8, 0xc1, 0x59, 0x1c, 0x8c, 0x8, 0xc8, 0xe4, 0x60, 0xac, 0x8e, 0x46, 0x4, 0x64, 0x72, 0x30, 0x56, 0x47, 0x23, + 0x2, 0x32, 0x39, 0x18, 0x2b, 0x23, 0x91, 0x81, 0x19, 0x1c, 0x8c, 0x15, 0x91, 0xc8, 0xc0, 0x8c, 0x8e, 0x46, 0xa, + 0xc8, 0xe4, 0x60, 0x46, 0x47, 0x23, 0x5, 0x64, 0x72, 0x30, 0x23, 0x23, 0x91, 0x82, 0xb2, 0x39, 0x18, 0x11, 0x91, + 0xc8, 0xc1, 0x59, 0x1c, 0x8c, 0x8, 0xc8, 0xe4, 0x60, 0xac, 0x8e, 0x46, 0x4, 0x64, 0x72, 0x30, 0x0, 0x4, 0x19, 0x48, + 0x0, 0x40, 0x8, 0x7, 0xff, 0x46, 0x24, 0x1, 0x0, 0x83, 0xef, 0xa, 0x4, 0x13, 0xe8, 0x42, 0x41, 0xf, 0xff, 0x30, + 0xe, 0x50, 0x41, 0xff, 0xe6, 0x0, 0x51, 0x41, 0x7, 0xe3, 0x0, 0x29, 0x41, 0x7, 0xe3, 0x0, 0x52, 0x82, 0xf, 0xff, + 0x30, 0x2, 0x8a, 0x8, 0x3f, 0x18, 0x1, 0x4a, 0x8, 0x3f, 0x18, 0x2, 0x94, 0x10, 0x7f, 0xf9, 0x80, 0x14, 0x50, 0x41, + 0xf8, 0xc0, 0xa, 0x50, 0x41, 0xf8, 0xc0, 0x14, 0xa0, 0x83, 0xff, 0xcc, 0x0, 0xa2, 0x82, 0xf, 0xc6, 0x0, 0x52, 0x82, + 0xf, 0xc6, 0x0, 0x0, 0x10, 0x7f, 0x88, 0x1, 0x46, 0xf8, 0x80, 0x53, 0x70, 0x80, 0xbb, 0x84, 0x13, 0x70, 0x84, 0x6e, + 0x10, 0x80, 0x9f, 0x10, 0x81, 0xbe, 0x21, 0x0, 0x1, 0x9, 0xff, 0xff, 0xff, 0xfd, 0x4b, 0xd, 0xb, 0xc8, 0x20, 0xf6, + 0x1a, 0x5e, 0x4c, 0x32, 0xc3, 0x5b, 0x9e, 0x44, 0x40, 0x5, 0xd, 0x6e, 0x79, 0x11, 0x0, 0x14, 0x37, 0xf8, 0x27, + 0x90, 0x0, 0x38, 0x6f, 0x2, 0x79, 0x0, 0x3, 0x86, 0xff, 0x4, 0xf2, 0x0, 0x7, 0xd, 0x73, 0xe4, 0x0, 0x0, 0x0, 0x8, + 0x6, 0x0, 0x0, 0x0, 0x8, 0x6, 0x0, 0xc3, 0xca, 0x30, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1d, 0x0, 0x4, 0x0, 0x13, 0x1, + 0x5, 0x0, 0x5, 0x0, 0x6, 0x49, 0xc9, 0x81, 0xf2, 0x39, 0x30, 0x23, 0x23, 0x91, 0x81, 0x19, 0x1c, 0x8c, 0x15, 0x91, + 0xc8, 0xc0, 0x8c, 0x8e, 0x46, 0xa, 0xc8, 0xe4, 0x60, 0x46, 0x47, 0x23, 0x5, 0x64, 0x72, 0x30, 0x23, 0x23, 0x91, + 0x82, 0xb2, 0x39, 0x18, 0x11, 0x91, 0xc8, 0xc1, 0x59, 0x1c, 0x8c, 0x8, 0xc8, 0xe4, 0x60, 0xac, 0x8e, 0x46, 0x4, + 0x64, 0x72, 0x30, 0x56, 0x47, 0x23, 0x2, 0x32, 0x39, 0x18, 0x2b, 0x23, 0x91, 0x81, 0x19, 0x1c, 0x8c, 0x15, 0x91, + 0xc8, 0xc0, 0x8c, 0x8e, 0x46, 0xa, 0xc8, 0xe4, 0x60, 0x46, 0x47, 0x23, 0x5, 0x64, 0x72, 0x30, 0x23, 0x23, 0x91, + 0x82, 0xb2, 0x39, 0x18, 0x11, 0x91, 0xc8, 0xc1, 0x59, 0x1c, 0x8c, 0x8, 0xc8, 0xe4, 0x60, 0xac, 0x8e, 0x46, 0x4, + 0x64, 0x72, 0x30, 0x0, 0x4, 0x19, 0x48, 0x0, 0x40, 0x8, 0x7, 0xff, 0x46, 0x24, 0x1, 0x0, 0x83, 0xef, 0xa, 0x4, + 0x13, 0xe8, 0x42, 0x41, 0xf, 0xff, 0x30, 0xe, 0x50, 0x41, 0xff, 0xe6, 0x0, 0x51, 0x41, 0x7, 0xe3, 0x0, 0x29, 0x41, + 0x7, 0xe3, 0x0, 0x52, 0x82, 0xf, 0xff, 0x30, 0x2, 0x8a, 0x8, 0x3f, 0x18, 0x1, 0x4a, 0x8, 0x3f, 0x18, 0x2, 0x94, + 0x10, 0x7f, 0xf9, 0x80, 0x14, 0x50, 0x41, 0xf8, 0xc0, 0xa, 0x50, 0x41, 0xf8, 0xc0, 0x14, 0xa0, 0x83, 0xff, 0xcc, + 0x0, 0xa2, 0x82, 0xf, 0xc6, 0x0, 0x52, 0x82, 0xf, 0xc6, 0x0, 0x0, 0x10, 0x7f, 0x88, 0x1, 0x46, 0xf8, 0x80, 0x53, + 0x70, 0x80, 0xbb, 0x84, 0x13, 0x70, 0x84, 0x6e, 0x10, 0x80, 0x9f, 0x10, 0x81, 0xbe, 0x21, 0x0, 0x1, 0x9, 0xff, + 0xff, 0xff, 0xfd, 0x4b, 0xd, 0xb, 0xc8, 0x20, 0xf6, 0x1a, 0x5e, 0x4c, 0x32, 0xc3, 0x5b, 0x9e, 0x44, 0x40, 0x5, 0xd, + 0x6e, 0x79, 0x11, 0x0, 0x14, 0x37, 0xf8, 0x27, 0x90, 0x0, 0x38, 0x6f, 0x2, 0x79, 0x0, 0x3, 0x86, 0xff, 0x4, 0xf2, + 0x0, 0x7, 0xd, 0x73, 0xe4, 0x0, 0x0, 0x0, 0x8, 0x6, 0x0, 0x0, 0x0, 0x8, 0x6, 0x0, 0xc3, 0xca, 0xde, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x1d, 0x0, 0x5, 0x0, 0xc1, 0x0, 0x5, 0x0, 0x5, 0x0, 0x6, 0x49, 0xc9, 0x81, 0xf2, 0x39, 0x30, 0x23, + 0x23, 0x91, 0x81, 0x19, 0x1c, 0x8c, 0x15, 0x91, 0xc8, 0xc0, 0x8c, 0x8e, 0x46, 0xa, 0xc8, 0xe4, 0x60, 0x46, 0x47, + 0x23, 0x5, 0x64, 0x72, 0x30, 0x23, 0xb, 0xc5, 0xe7, 0xe2, 0xf3, 0xf1, 0x79, 0xf8, 0xbc, 0xfc, 0x5e, 0x7e, 0x2f, + 0x3f, 0x17, 0x9f, 0x8b, 0xcf, 0xc5, 0xe7, 0xe2, 0xf3, 0xf1, 0x78, 0xc, 0x1e, 0x81, 0xd0, 0x74, 0x3a, 0x18, 0x0, + 0xb, 0xf5, 0x20, 0x1, 0x0, 0x20, 0x1f, 0xfd, 0x31, 0x20, 0x8, 0x4, 0x1f, 0x54, 0x28, 0x10, 0x47, 0xd0, 0x84, 0x82, + 0x13, 0xe8, 0x19, 0x21, 0xf, 0xff, 0x30, 0x5, 0xa4, 0x81, 0x52, 0x7d, 0x2e, 0x97, 0x5f, 0x4b, 0xcf, 0xc5, 0xe7, + 0xe2, 0xf0, 0x60, 0x40, 0x80, 0x8a, 0x1e, 0x8f, 0x4c, 0x0, 0x4, 0x4, 0x85, 0x9, 0x6f, 0xff, 0xfa, 0x1, 0x4d, 0xf1, + 0x2, 0xb7, 0xa8, 0x14, 0x84, 0x92, 0x5, 0xa6, 0x16, 0x84, 0x16, 0x8c, 0x5e, 0x1, 0x24, 0x3f, 0xff, 0x80, 0x98, + 0xe1, 0x2, 0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf2, 0xb0, 0x1a, 0x21, 0xe4, 0x4, 0x35, 0x80, 0xd2, + 0x3c, 0x84, 0x30, 0x3, 0x81, 0xa9, 0xa3, 0xc7, 0x80, 0xc, 0x0, 0x41, 0x24, 0x20, 0x0, 0x5, 0x12, 0xaa, 0xa0, 0x1, + 0xc4, 0xe5, 0x54, 0x0, 0x72, 0x72, 0xaa, 0x0, 0x72, 0x4b, 0x50, 0x0, 0x0, 0x8, 0x6, 0x0, 0x0, 0x0, 0x8, 0x6, 0x0, + 0xc5, 0xcc, 0x8, 0x0, 0x0, 0x0, 0x1, 0x0, +]; + +const FRAME_MARKER_PDU: SurfaceCommand<'_> = SurfaceCommand::FrameMarker(FrameMarkerPdu { + frame_action: FrameAction::Begin, + frame_id: Some(5), +}); + +static SURFACE_BITS_PDU: LazyLock> = LazyLock::new(|| { + SurfaceCommand::StreamSurfaceBits(SurfaceBitsPdu { + destination: ExclusiveRectangle { + left: 0, + top: 0, + right: 1920, + bottom: 1080, + }, + extended_bitmap_data: ExtendedBitmapDataPdu { + bpp: 32, + codec_id: 3, + width: 1920, + height: 1080, + header: None, + data: &SURFACE_BITS_BUFFER[22..], + }, + }) +}); + +#[test] +fn from_buffer_correctly_parses_surface_command_frame_marker() { + assert_eq!( + FRAME_MARKER_PDU, + decode::>(FRAME_MARKER_BUFFER.as_ref()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_surface_command_frame_marker() { + let expected = FRAME_MARKER_BUFFER.as_ref(); + let mut buffer = vec![0; expected.len()]; + + encode(&FRAME_MARKER_PDU, buffer.as_mut_slice()).unwrap(); + assert_eq!(expected, buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_surface_command_frame_marker() { + assert_eq!(FRAME_MARKER_BUFFER.len(), FRAME_MARKER_PDU.size()); +} + +#[test] +fn from_buffer_correctly_parses_surface_command_bits() { + assert_eq!( + *SURFACE_BITS_PDU, + decode::>(SURFACE_BITS_BUFFER.as_ref()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_surface_command_bits() { + let expected = SURFACE_BITS_BUFFER.as_ref(); + let mut buffer = vec![0; expected.len()]; + + encode(&*SURFACE_BITS_PDU, buffer.as_mut_slice()).unwrap(); + assert_eq!(expected, buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_surface_command_bits() { + assert_eq!(SURFACE_BITS_BUFFER.len(), SURFACE_BITS_PDU.size()); +} diff --git a/crates/ironrdp-pdu/src/ber.rs b/crates/ironrdp-pdu/src/ber.rs new file mode 100644 index 00000000..bda4e0fc --- /dev/null +++ b/crates/ironrdp-pdu/src/ber.rs @@ -0,0 +1,743 @@ +use ironrdp_core::{cast_length, ensure_size, invalid_field_err, ReadCursor, WriteCursor}; + +use crate::{DecodeResult, EncodeResult}; + +#[repr(u8)] +#[derive(Copy, Clone)] +pub(crate) enum Pc { + Primitive = 0x00, + Construct = 0x20, +} + +impl Pc { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +#[repr(u8)] +#[expect(unused)] +#[derive(Copy, Clone)] +enum Class { + Universal = 0x00, + Application = 0x40, + ContextSpecific = 0x80, + Private = 0xC0, +} + +impl Class { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +#[repr(u8)] +#[expect(unused)] +#[derive(Copy, Clone)] +enum Tag { + Mask = 0x1F, + Boolean = 0x01, + Integer = 0x02, + BitString = 0x03, + OctetString = 0x04, + ObjectIdentifier = 0x06, + Enumerated = 0x0A, + Sequence = 0x10, +} + +impl Tag { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +pub(crate) const SIZEOF_ENUMERATED: usize = 3; +pub(crate) const SIZEOF_BOOL: usize = 3; + +const TAG_MASK: u8 = 0x1F; + +pub(crate) fn sizeof_application_tag(tagnum: u8, length: u16) -> usize { + let tag_len = if tagnum > 0x1E { 2 } else { 1 }; + + sizeof_length(length) + tag_len +} + +pub(crate) fn sizeof_sequence_tag(length: u16) -> usize { + 1 + sizeof_length(length) +} + +pub(crate) fn sizeof_octet_string(length: u16) -> usize { + 1 + sizeof_length(length) + usize::from(length) +} + +pub(crate) fn sizeof_integer(value: u32) -> usize { + if value < 0x0000_0080 { + 3 + } else if value < 0x0000_8000 { + 4 + } else if value < 0x0080_0000 { + 5 + } else { + 6 + } +} + +pub(crate) fn write_sequence_tag(stream: &mut WriteCursor<'_>, length: u16) -> EncodeResult { + write_universal_tag(stream, Tag::Sequence, Pc::Construct)?; + + write_length(stream, length).map(|length| length + 1) +} + +pub(crate) fn read_sequence_tag(stream: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: stream, size: 1); + let identifier = stream.read_u8(); + + if identifier != Class::Universal.as_u8() | Pc::Construct.as_u8() | (TAG_MASK & Tag::Sequence.as_u8()) { + Err(invalid_field_err!("identifier", "invalid sequence tag identifier")) + } else { + read_length(stream) + } +} + +pub(crate) fn write_application_tag(stream: &mut WriteCursor<'_>, tagnum: u8, length: u16) -> EncodeResult { + ensure_size!(in: stream, size: sizeof_application_tag(tagnum, length)); + + let taglen = if tagnum > 0x1E { + stream.write_u8(Class::Application.as_u8() | Pc::Construct.as_u8() | TAG_MASK); + stream.write_u8(tagnum); + 2 + } else { + stream.write_u8(Class::Application.as_u8() | Pc::Construct.as_u8() | (TAG_MASK & tagnum)); + 1 + }; + + write_length(stream, length).map(|length| length + taglen) +} + +pub(crate) fn read_application_tag(stream: &mut ReadCursor<'_>, tagnum: u8) -> DecodeResult { + ensure_size!(in: stream, size: 1); + let identifier = stream.read_u8(); + + if tagnum > 0x1E { + if identifier != Class::Application.as_u8() | Pc::Construct.as_u8() | TAG_MASK { + return Err(invalid_field_err!("identifier", "invalid application tag identifier")); + } + ensure_size!(in: stream, size: 1); + if stream.read_u8() != tagnum { + return Err(invalid_field_err!("tagnum", "invalid application tag identifier")); + } + } else if identifier != Class::Application.as_u8() | Pc::Construct.as_u8() | (TAG_MASK & tagnum) { + return Err(invalid_field_err!("identifier", "invalid application tag identifier")); + } + + read_length(stream) +} + +pub(crate) fn write_enumerated(stream: &mut WriteCursor<'_>, enumerated: u8) -> EncodeResult { + let mut size = 0; + size += write_universal_tag(stream, Tag::Enumerated, Pc::Primitive)?; + size += write_length(stream, 1)?; + ensure_size!(in: stream, size: 1); + stream.write_u8(enumerated); + size += 1; + + Ok(size) +} + +pub(crate) fn read_enumerated(stream: &mut ReadCursor<'_>, count: u8) -> DecodeResult { + read_universal_tag(stream, Tag::Enumerated, Pc::Primitive)?; + + let length = read_length(stream)?; + if length != 1 { + return Err(invalid_field_err!("len", "invalid enumerated len")); + } + + ensure_size!(in: stream, size: 1); + let enumerated = stream.read_u8(); + if enumerated == u8::MAX || enumerated + 1 > count { + return Err(invalid_field_err!("enumerated", "invalid enumerated value")); + } + + Ok(enumerated) +} + +pub(crate) fn write_integer(stream: &mut WriteCursor<'_>, value: u32) -> EncodeResult { + write_universal_tag(stream, Tag::Integer, Pc::Primitive)?; + + if value < 0x0000_0080 { + write_length(stream, 1)?; + ensure_size!(in: stream, size: 1); + stream.write_u8(u8::try_from(value).expect("value is guaranteed to fit into u8 due to the prior check")); + + Ok(3) + } else if value < 0x0000_8000 { + write_length(stream, 2)?; + ensure_size!(in: stream, size: 2); + stream.write_u16_be(u16::try_from(value).expect("value is guaranteed to fit into u16 due to the prior check")); + + Ok(4) + } else if value < 0x0080_0000 { + write_length(stream, 3)?; + ensure_size!(in: stream, size: 3); + stream.write_u8(u8::try_from(value >> 16).expect("value is guaranteed to fit into u8 due to the prior check")); + stream.write_u16_be( + u16::try_from(value & 0xFFFF).expect("masking with 0xFFFF ensures that the value fits into u16"), + ); + + Ok(5) + } else { + write_length(stream, 4)?; + ensure_size!(in: stream, size: 4); + stream.write_u32_be(value); + + Ok(6) + } +} + +pub(crate) fn read_integer(stream: &mut ReadCursor<'_>) -> DecodeResult { + read_universal_tag(stream, Tag::Integer, Pc::Primitive)?; + let length = read_length(stream)?; + + if length == 1 { + ensure_size!(in: stream, size: 1); + Ok(u64::from(stream.read_u8())) + } else if length == 2 { + ensure_size!(in: stream, size: 2); + Ok(u64::from(stream.read_u16_be())) + } else if length == 3 { + ensure_size!(in: stream, size: 3); + let a = stream.read_u8(); + let b = stream.read_u16_be(); + + Ok(u64::from(b) + (u64::from(a) << 16)) + } else if length == 4 { + ensure_size!(in: stream, size: 4); + Ok(u64::from(stream.read_u32_be())) + } else if length == 8 { + ensure_size!(in: stream, size: 8); + Ok(stream.read_u64_be()) + } else { + Err(invalid_field_err!("len", "invalid integer len")) + } +} + +pub(crate) fn write_bool(stream: &mut WriteCursor<'_>, value: bool) -> EncodeResult { + let mut size = 0; + size += write_universal_tag(stream, Tag::Boolean, Pc::Primitive)?; + size += write_length(stream, 1)?; + + ensure_size!(in: stream, size: 1); + stream.write_u8(if value { 0xFF } else { 0x00 }); + size += 1; + + Ok(size) +} + +pub(crate) fn read_bool(stream: &mut ReadCursor<'_>) -> DecodeResult { + read_universal_tag(stream, Tag::Boolean, Pc::Primitive)?; + let length = read_length(stream)?; + + if length != 1 { + return Err(invalid_field_err!("len", "invalid integer len")); + } + + ensure_size!(in: stream, size: 1); + Ok(stream.read_u8() != 0) +} + +pub(crate) fn write_octet_string(stream: &mut WriteCursor<'_>, value: &[u8]) -> EncodeResult { + let tag_size = write_octet_string_tag(stream, cast_length!("len", value.len())?)?; + ensure_size!(in: stream, size: value.len()); + stream.write_slice(value); + Ok(tag_size + value.len()) +} + +pub(crate) fn write_octet_string_tag(stream: &mut WriteCursor<'_>, length: u16) -> EncodeResult { + write_universal_tag(stream, Tag::OctetString, Pc::Primitive)?; + write_length(stream, length).map(|length| length + 1) +} + +pub(crate) fn read_octet_string(stream: &mut ReadCursor<'_>) -> DecodeResult> { + let length = cast_length!("len", read_octet_string_tag(stream)?)?; + + ensure_size!(in: stream, size: length); + let buffer = stream.read_slice(length); + + Ok(buffer.into()) +} + +pub(crate) fn read_octet_string_tag(stream: &mut ReadCursor<'_>) -> DecodeResult { + read_universal_tag(stream, Tag::OctetString, Pc::Primitive)?; + read_length(stream) +} + +fn write_universal_tag(stream: &mut WriteCursor<'_>, tag: Tag, pc: Pc) -> EncodeResult { + ensure_size!(in: stream, size: 1); + + let identifier = Class::Universal.as_u8() | pc.as_u8() | (TAG_MASK & tag.as_u8()); + stream.write_u8(identifier); + + Ok(1) +} + +fn read_universal_tag(stream: &mut ReadCursor<'_>, tag: Tag, pc: Pc) -> DecodeResult<()> { + ensure_size!(in: stream, size: 1); + + let identifier = stream.read_u8(); + + if identifier != Class::Universal.as_u8() | pc.as_u8() | (TAG_MASK & tag.as_u8()) { + Err(invalid_field_err!("identifier", "invalid universal tag identifier")) + } else { + Ok(()) + } +} + +fn write_length(stream: &mut WriteCursor<'_>, length: u16) -> EncodeResult { + ensure_size!(in: stream, size: sizeof_length(length)); + + if length > 0xFF { + stream.write_u8(0x80 ^ 0x2); + stream.write_u16_be(length); + + Ok(3) + } else if length > 0x7F { + stream.write_u8(0x80 ^ 0x1); + stream.write_u8(u8::try_from(length).expect("length is guaranteed to fit into u8 due to the prior check")); + + Ok(2) + } else { + stream.write_u8(u8::try_from(length).expect("length is guaranteed to fit into u8 due to the prior check")); + + Ok(1) + } +} + +fn read_length(stream: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: stream, size: 1); + let byte = stream.read_u8(); + + if byte & 0x80 != 0 { + let len = byte & !0x80; + + if len == 1 { + ensure_size!(in: stream, size: 1); + Ok(u16::from(stream.read_u8())) + } else if len == 2 { + ensure_size!(in: stream, size: 2); + Ok(stream.read_u16_be()) + } else { + Err(invalid_field_err!("len", "invalid length of the length")) + } + } else { + Ok(u16::from(byte)) + } +} + +fn sizeof_length(length: u16) -> usize { + if length > 0xff { + 3 + } else if length > 0x7f { + 2 + } else { + 1 + } +} + +#[cfg(test)] +mod tests { + use ironrdp_core::DecodeErrorKind; + + use super::*; + + #[test] + fn write_sequence_tag_is_correct() { + let mut buf = [0x0; 4]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_sequence_tag(&mut cur, 0x100).unwrap(), 4); + assert_eq!(buf, [0x30, 0x82, 0x01, 0x00]); + } + + #[test] + fn read_sequence_tag_returns_correct_length() { + let buf = [0x30, 0x82, 0x01, 0x00]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_sequence_tag(&mut cur).unwrap(), 0x100); + } + + #[test] + fn read_sequence_tag_returns_error_on_invalid_tag() { + let buf = [0x3a, 0x82, 0x01, 0x00]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_sequence_tag(&mut cur).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn write_application_tag_is_correct_with_long_tag() { + let mut buf = [0x0; 3]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_application_tag(&mut cur, 0x1F, 0x0F).unwrap(), 3); + assert_eq!(buf, [0x7F, 0x1F, 0x0F]); + } + + #[test] + fn write_application_tag_is_correct_with_short_tag() { + let mut buf = [0x0; 4]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_application_tag(&mut cur, 0x08, 0x100).unwrap(), 4); + assert_eq!(buf, [0x68, 0x82, 0x01, 0x00]); + } + + #[test] + fn read_application_tag_is_correct_with_long_tag() { + let buf = [0x7F, 0x1F, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_application_tag(&mut cur, 0x1F).unwrap(), 0x0F); + } + + #[test] + fn read_application_tag_is_correct_with_short_tag() { + let buf = [0x68, 0x82, 0x01, 0x00]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_application_tag(&mut cur, 0x08).unwrap(), 0x100); + } + + #[test] + fn read_application_tag_returns_error_on_invalid_long_tag() { + let buf = [0x68, 0x1B, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_application_tag(&mut cur, 0x1F).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn read_application_tag_returns_error_on_invalid_long_tag_value() { + let buf = [0x7F, 0x1B, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_application_tag(&mut cur, 0x1F).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn read_application_tag_returns_error_on_invalid_short_tag() { + let buf = [0x67, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_application_tag(&mut cur, 0x08).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn write_enumerated_is_correct() { + let mut buf = [0x0; 3]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_enumerated(&mut cur, 0x0F).unwrap(), 3); + assert_eq!(buf, [0x0A, 0x01, 0x0F]); + } + + #[test] + fn read_enumerated_is_correct() { + let buf = [0x0A, 0x01, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_enumerated(&mut cur, 0x10).unwrap(), 0x0F); + } + + #[test] + fn read_enumerated_returns_error_on_wrong_tag() { + let buf = [0x0B, 0x01, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_enumerated(&mut cur, 0x10).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn read_enumerated_returns_error_on_invalid_len() { + let buf = [0x0A, 0x02, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_enumerated(&mut cur, 0x10).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn read_enumerated_returns_error_on_invalid_variant() { + let buf = [0x0A, 0x01, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_enumerated(&mut cur, 0x05).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn write_bool_true_is_correct() { + let mut buf = [0x0; 3]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_bool(&mut cur, true).unwrap(), 3); + assert_eq!(buf, [0x01, 0x01, 0xFF]); + } + + #[test] + fn write_bool_false_is_correct() { + let mut buf = [0x0; 3]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_bool(&mut cur, false).unwrap(), 3); + assert_eq!(buf, [0x01, 0x01, 0x00]); + } + + #[test] + fn read_bool_true_is_correct() { + let buf = vec![0x01, 0x01, 0xFF]; + let mut cur = ReadCursor::new(&buf); + assert!(read_bool(&mut cur).unwrap()); + } + + #[test] + fn read_bool_false_is_correct() { + let buf = [0x01, 0x01, 0x00]; + let mut cur = ReadCursor::new(&buf); + assert!(!read_bool(&mut cur).unwrap()); + } + + #[test] + fn read_bool_returns_error_on_wrong_tag() { + let buf = [0x0A, 0x01, 0xFF]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_bool(&mut cur).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn read_bool_returns_error_on_invalid_len() { + let buf = [0x01, 0x02, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_bool(&mut cur).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn write_octet_string_tag_is_correct() { + let mut buf = [0x0; 2]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_octet_string_tag(&mut cur, 0x0F).unwrap(), 2); + assert_eq!(buf, [0x04, 0x0F]); + } + + #[test] + fn read_octet_string_tag_is_correct() { + let buf = [0x04, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_octet_string_tag(&mut cur).unwrap(), 0x0F); + } + + #[test] + fn read_octet_string_tag_returns_error_on_wrong_tag() { + let buf = [0x05, 0x0F]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_octet_string_tag(&mut cur).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn write_octet_string_is_correct() { + let mut buf = [0x0; 7]; + let mut cur = WriteCursor::new(&mut buf); + let string = [0x68, 0x65, 0x6c, 0x6c, 0x6f]; + let expected: Vec<_> = [0x04, 0x05].iter().chain(string.iter()).copied().collect(); + assert_eq!(write_octet_string(&mut cur, &string).unwrap(), 7); + assert_eq!(buf, expected.as_slice()); + } + + #[test] + fn read_octet_string_is_correct() { + let buf = [0x04, 0x03, 0x00, 0x01, 0x02]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_octet_string(&mut cur).unwrap(), vec![0x00, 0x01, 0x02]); + } + + #[test] + fn write_length_is_correct_with_3_byte_length() { + let mut buf = [0x0; 3]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_length(&mut cur, 0x100).unwrap(), 3); + assert_eq!(buf, [0x82, 0x01, 0x00]); + } + + #[test] + fn write_length_is_correct_with_2_byte_length() { + let mut buf = [0x0; 2]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_length(&mut cur, 0xFA).unwrap(), 2); + assert_eq!(buf, [0x81, 0xFA]); + } + + #[test] + fn write_length_is_correct_with_1_byte_length() { + let mut buf = [0x0]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_length(&mut cur, 0x70).unwrap(), 1); + assert_eq!(buf, [0x70]); + } + + #[test] + fn read_length_is_correct_with_3_byte_length() { + let buf = [0x82, 0x01, 0x00]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_length(&mut cur).unwrap(), 0x100); + } + + #[test] + fn read_length_is_correct_with_2_byte_length() { + let buf = [0x81, 0xFA]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_length(&mut cur).unwrap(), 0xFA); + } + + #[test] + fn read_length_is_correct_with_1_byte_length() { + let buf = [0x70]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_length(&mut cur).unwrap(), 0x70); + } + + #[test] + fn read_length_returns_error_on_invalid_length() { + let buf = [0x8a, 0x1]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_length(&mut cur).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn write_integer_is_correct_with_4_byte_integer() { + let mut buf = [0x0; 6]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_integer(&mut cur, 0x0080_0000).unwrap(), 6); + assert_eq!(buf, [0x02, 0x04, 0x00, 0x80, 0x00, 0x00]); + } + + #[test] + fn write_integer_is_correct_with_3_byte_integer() { + let mut buf = [0x0; 5]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_integer(&mut cur, 0x80000).unwrap(), 5); + assert_eq!(buf, [0x02, 0x03, 0x08, 0x00, 0x00]); + } + + #[test] + fn write_integer_is_correct_with_2_byte_integer() { + let mut buf = [0x0; 4]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_integer(&mut cur, 0x800).unwrap(), 4); + assert_eq!(buf, [0x02, 0x02, 0x08, 0x00]); + } + + #[test] + fn write_integer_is_correct_with_1_byte_integer() { + let mut buf = [0x0; 3]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_integer(&mut cur, 0x79).unwrap(), 3); + assert_eq!(buf, [0x02, 0x01, 0x79]); + } + + #[test] + fn read_integer_is_correct_with_8_byte_integer() { + let buf = [0x02, 0x08, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_integer(&mut cur).unwrap(), 0x0080_0000_0000_0000); + } + + #[test] + fn read_integer_is_correct_with_4_byte_integer() { + let buf = [0x02, 0x04, 0x00, 0x80, 0x00, 0x00]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_integer(&mut cur).unwrap(), 0x0080_0000); + } + + #[test] + fn read_integer_is_correct_with_3_byte_integer() { + let buf = [0x02, 0x03, 0x08, 0x00, 0x00]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_integer(&mut cur).unwrap(), 0x80000); + } + + #[test] + fn read_integer_is_correct_with_2_byte_integer() { + let buf = [0x02, 0x02, 0x08, 0x00]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_integer(&mut cur).unwrap(), 0x800); + } + + #[test] + fn read_integer_is_correct_with_1_byte_integer() { + let buf = [0x02, 0x01, 0x79]; + let mut cur = ReadCursor::new(&buf); + assert_eq!(read_integer(&mut cur).unwrap(), 0x79); + } + + #[test] + fn read_integer_returns_error_on_incorrect_len() { + let buf = [0x02, 0x06, 0x79]; + let mut cur = ReadCursor::new(&buf); + assert!(matches!( + read_integer(&mut cur).unwrap_err().kind(), + DecodeErrorKind::InvalidField { .. } + )); + } + + #[test] + fn write_universal_tag_primitive_integer_is_correct() { + let mut buf = [0x0]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!(write_universal_tag(&mut cur, Tag::Integer, Pc::Primitive).unwrap(), 1); + assert_eq!(buf, [0x02]); + } + + #[test] + fn write_universal_tag_construct_enumerated_is_correct() { + let mut buf = [0x0]; + let mut cur = WriteCursor::new(&mut buf); + assert_eq!( + write_universal_tag(&mut cur, Tag::Enumerated, Pc::Construct).unwrap(), + 1 + ); + assert_eq!(buf, [0x2A]); + } + + #[test] + fn sizeof_length_with_long_len() { + let len = 625; + let expected = 3; + assert_eq!(sizeof_length(len), expected); + } +} diff --git a/crates/ironrdp-pdu/src/codecs/mod.rs b/crates/ironrdp-pdu/src/codecs/mod.rs new file mode 100644 index 00000000..6fb4906b --- /dev/null +++ b/crates/ironrdp-pdu/src/codecs/mod.rs @@ -0,0 +1 @@ +pub mod rfx; diff --git a/crates/ironrdp-pdu/src/codecs/rfx/data_messages.rs b/crates/ironrdp-pdu/src/codecs/rfx/data_messages.rs new file mode 100644 index 00000000..d7da57b2 --- /dev/null +++ b/crates/ironrdp-pdu/src/codecs/rfx/data_messages.rs @@ -0,0 +1,701 @@ +use core::iter; + +use bit_field::BitField as _; +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use crate::codecs::rfx::Block; + +const CONTEXT_ID: u8 = 0; +const TILE_SIZE: u16 = 0x0040; +const COLOR_CONVERSION_ICT: u16 = 1; +const CLW_XFORM_DWT_53_A: u16 = 1; +const SCALAR_QUANTIZATION: u16 = 1; +const LRF: bool = true; +const CBT_REGION: u16 = 0xcac1; +const NUMBER_OF_TILESETS: u16 = 1; +const CBT_TILESET: u16 = 0xcac2; +const IDX: u16 = 0; +const IS_LAST_TILESET_FLAG: bool = true; +const RECTANGLE_SIZE: usize = 8; + +/// [2.2.2.2.4] TS_RFX_CONTEXT +/// +/// [2.2.2.2.4]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/bde1ce78-5d9e-44c1-8a15-5843fa12270a +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ContextPdu { + pub flags: OperatingMode, + pub entropy_algorithm: EntropyAlgorithm, +} + +impl ContextPdu { + const NAME: &'static str = "RfxContext"; + + const FIXED_PART_SIZE: usize = 1 /* ctxId */ + 2 /* tileSize */ + 2 /* properties */; +} + +impl Encode for ContextPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u8(CONTEXT_ID); + dst.write_u16(TILE_SIZE); + + let mut properties: u16 = 0; + properties.set_bits(0..3, self.flags.bits()); + properties.set_bits(3..5, COLOR_CONVERSION_ICT); + properties.set_bits(5..9, CLW_XFORM_DWT_53_A); + properties.set_bits(9..13, self.entropy_algorithm.as_u16()); + properties.set_bits(13..15, SCALAR_QUANTIZATION); + properties.set_bit(15, false); // reserved + dst.write_u16(properties); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ContextPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let id = src.read_u8(); + if id != CONTEXT_ID { + return Err(invalid_field_err!("ctxId", "Invalid context ID")); + } + + let tile_size = src.read_u16(); + if tile_size != TILE_SIZE { + return Err(invalid_field_err!("tileSize", "Invalid tile size")); + } + + let properties = src.read_u16(); + let flags = OperatingMode::from_bits_truncate(properties.get_bits(0..3)); + let color_conversion_transform = properties.get_bits(3..5); + if color_conversion_transform != COLOR_CONVERSION_ICT { + return Err(invalid_field_err!("cct", "Invalid color conversion transform")); + } + + let dwt = properties.get_bits(5..9); + if dwt != CLW_XFORM_DWT_53_A { + return Err(invalid_field_err!("dwt", "Invalid DWT")); + } + + let entropy_algorithm_bits = properties.get_bits(9..13); + let entropy_algorithm = EntropyAlgorithm::from_u16(entropy_algorithm_bits) + .ok_or_else(|| invalid_field_err!("entropy_algorithm", "Invalid entropy algorithm"))?; + + let quantization_type = properties.get_bits(13..15); + if quantization_type != SCALAR_QUANTIZATION { + return Err(invalid_field_err!("qt", "Invalid quantization type")); + } + + let _reserved = properties.get_bit(15); + + Ok(Self { + flags, + entropy_algorithm, + }) + } +} + +/// [2.2.2.3.1] TS_RFX_FRAME_BEGIN +/// +/// [2.2.2.3.1]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/7a938a26-3fc2-436b-bc84-09dfff59b5e7 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FrameBeginPdu { + pub index: u32, + pub number_of_regions: i16, +} + +impl FrameBeginPdu { + const NAME: &'static str = "RfxFrameBegin"; + + const FIXED_PART_SIZE: usize = 4 /* frameIdx */ + 2 /* numRegions */; +} + +impl Encode for FrameBeginPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.index); + dst.write_i16(self.number_of_regions); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for FrameBeginPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let index = src.read_u32(); + let number_of_regions = src.read_i16(); + + Ok(Self { + index, + number_of_regions, + }) + } +} + +/// [2.2.2.3.2] TS_RFX_FRAME_END +/// +/// [2.2.2.3.1]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/b4cb2676-0268-450b-ad32-72f66d0598e8 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FrameEndPdu; + +impl FrameEndPdu { + const NAME: &'static str = "RfxFrameEnd"; + + const FIXED_PART_SIZE: usize = 0; +} + +impl Encode for FrameEndPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for FrameEndPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + Ok(Self) + } +} + +/// [2.2.2.3.3] TS_RFX_REGION +/// +/// [2.2.2.3.3]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/23d2a1d6-1be0-4357-83eb-998b66ddd4d9 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RegionPdu { + pub rectangles: Vec, +} + +impl RegionPdu { + const NAME: &'static str = "RfxRegion"; + + const FIXED_PART_SIZE: usize = 1 /* regionFlags */ + 2 /* numRects */ + 2 /* regionType */ + 2 /* numTilesets */; +} + +impl Encode for RegionPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let mut region_flags = 0; + region_flags.set_bit(0, LRF); + dst.write_u8(region_flags); + + dst.write_u16(cast_length!("numRectangles", self.rectangles.len())?); + for rectangle in self.rectangles.iter() { + rectangle.encode(dst)?; + } + + dst.write_u16(CBT_REGION); + dst.write_u16(NUMBER_OF_TILESETS); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.rectangles.len() * RECTANGLE_SIZE + } +} + +impl<'de> Decode<'de> for RegionPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let region_flags = src.read_u8(); + let lrf = region_flags.get_bit(0); + if lrf != LRF { + return Err(invalid_field_err!("lrf", "Invalid lrf")); + } + + let number_of_rectangles = usize::from(src.read_u16()); + + ensure_size!(in: src, size: number_of_rectangles * RECTANGLE_SIZE); + + let rectangles = iter::repeat_with(|| RfxRectangle::decode(src)) + .take(number_of_rectangles) + .collect::, _>>()?; + + ensure_size!(in: src, size: 4); + + let region_type = src.read_u16(); + if region_type != CBT_REGION { + return Err(invalid_field_err!("regionType", "Invalid region type")); + } + + let number_of_tilesets = src.read_u16(); + if number_of_tilesets != NUMBER_OF_TILESETS { + return Err(invalid_field_err!("numTilesets", "Invalid number of tilesets")); + } + + Ok(Self { rectangles }) + } +} + +/// [2.2.2.3.4] TS_RFX_TILESET +/// +/// [2.2.2.3.4] https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/7c926114-4bea-4c69-a9a1-caa6e88847a6 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TileSetPdu<'a> { + pub entropy_algorithm: EntropyAlgorithm, + pub quants: Vec, + pub tiles: Vec>, +} + +impl TileSetPdu<'_> { + const NAME: &'static str = "RfxTileSet"; + + const FIXED_PART_SIZE: usize = 2 /* subtype */ + 2 /* idx */ + 2 /* properties */ + 1 /* numQuant */ + 1 /* tileSize */+ 2 /* numTiles */ + 4 /* tilesDataSize */; +} + +impl Encode for TileSetPdu<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(CBT_TILESET); + dst.write_u16(IDX); + + let mut properties: u16 = 0; + properties.set_bit(0, IS_LAST_TILESET_FLAG); + properties.set_bits(1..4, OperatingMode::empty().bits()); // The decoder MUST ignore this flag + properties.set_bits(4..6, COLOR_CONVERSION_ICT); + properties.set_bits(6..10, CLW_XFORM_DWT_53_A); + properties.set_bits(10..14, self.entropy_algorithm.as_u16()); + properties.set_bits(14..16, SCALAR_QUANTIZATION); + dst.write_u16(properties); + + dst.write_u8(cast_length!("numQuant", self.quants.len())?); + dst.write_u8(u8::try_from(TILE_SIZE).expect("TILE_SIZE value fits into u8")); + dst.write_u16(cast_length!("numTiles", self.tiles.len())?); + + let tiles_data_size = self.tiles.iter().map(|t| Block::Tile(t.clone()).size()).sum::(); + dst.write_u32(cast_length!("tilesDataSize", tiles_data_size)?); + + for quant in &self.quants { + quant.encode(dst)?; + } + + for tile in &self.tiles { + Block::Tile(tile.clone()).encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + self.quants.iter().map(Encode::size).sum::() + + self.tiles.iter().map(|t| Block::Tile(t.clone()).size()).sum::() + } +} + +impl<'de> Decode<'de> for TileSetPdu<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let subtype = src.read_u16(); + if subtype != CBT_TILESET { + return Err(invalid_field_err!("subtype", "Invalid message type")); + } + + let id_of_context = src.read_u16(); + if id_of_context != IDX { + return Err(invalid_field_err!("id_of_context", "Invalid RFX context")); + } + + let properties = src.read_u16(); + let is_last = properties.get_bit(0); + if is_last != IS_LAST_TILESET_FLAG { + return Err(invalid_field_err!("last", "Invalid last flag")); + } + + // The encoder MUST set `flags` value to the value of flags + // that is set in the properties field of TS_RFX_CONTEXT. + // The decoder MUST ignore this flag and MUST use the flags specified + // in the flags field of the TS_RFX_CONTEXT. + + let color_conversion_transform = properties.get_bits(4..6); + if color_conversion_transform != COLOR_CONVERSION_ICT { + return Err(invalid_field_err!("cct", "Invalid color conversion")); + } + + let dwt = properties.get_bits(6..10); + if dwt != CLW_XFORM_DWT_53_A { + return Err(invalid_field_err!("xft", "Invalid DWT")); + } + + let entropy_algorithm_bits = properties.get_bits(10..14); + let entropy_algorithm = EntropyAlgorithm::from_u16(entropy_algorithm_bits) + .ok_or_else(|| invalid_field_err!("entropy", "Invalid entropy algorithm"))?; + + let quantization_type = properties.get_bits(14..16); + if quantization_type != SCALAR_QUANTIZATION { + return Err(invalid_field_err!("scalar", "Invalid quantization type")); + } + + let number_of_quants = usize::from(src.read_u8()); + + let tile_size = u16::from(src.read_u8()); + if tile_size != TILE_SIZE { + return Err(invalid_field_err!("tile_size", "Invalid tile size")); + } + + let number_of_tiles = usize::from(src.read_u16()); + let _tiles_data_size = src.read_u32(); + + let quants = iter::repeat_with(|| Quant::decode(src)) + .take(number_of_quants) + .collect::, _>>()?; + + let tiles = iter::repeat_with(|| Block::decode(src)) + .take(number_of_tiles) + .collect::, _>>()?; + + let tiles = tiles + .into_iter() + .map(|b| match b { + Block::Tile(tile) => Ok(tile), + _ => Err(invalid_field_err!("tile", "Invalid block type, expected Tile")), + }) + .collect::, _>>()?; + + Ok(Self { + entropy_algorithm, + quants, + tiles, + }) + } +} +/// [2.2.2.1.6] TS_RFX_RECT +/// +/// [2.2.2.1.6]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/26eb819a-955b-4b08-b3a0-997231170059 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RfxRectangle { + pub x: u16, + pub y: u16, + pub width: u16, + pub height: u16, +} + +impl RfxRectangle { + const NAME: &'static str = "RfxRectangle"; + + const FIXED_PART_SIZE: usize = 4 * 2 /* x, y, width, height */; +} + +impl Encode for RfxRectangle { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.x); + dst.write_u16(self.y); + dst.write_u16(self.width); + dst.write_u16(self.height); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for RfxRectangle { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let x = src.read_u16(); + let y = src.read_u16(); + let width = src.read_u16(); + let height = src.read_u16(); + + Ok(Self { x, y, width, height }) + } +} + +/// 2.2.2.1.5 TS_RFX_CODEC_QUANT +/// +/// [2.2.2.1.5]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/3e9c8af4-7539-4c9d-95de-14b1558b902c +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Quant { + pub ll3: u8, + pub lh3: u8, + pub hl3: u8, + pub hh3: u8, + pub lh2: u8, + pub hl2: u8, + pub hh2: u8, + pub lh1: u8, + pub hl1: u8, + pub hh1: u8, +} + +// The quantization values control the compression rate and quality. The value +// range is between 6 and 15. The higher value, the higher compression rate and +// lower quality. +// +// This is the default values being use by the MS RDP server, and we will also +// use it as our default values for the encoder. +impl Default for Quant { + fn default() -> Self { + Self { + ll3: 6, + lh3: 6, + hl3: 6, + hh3: 6, + lh2: 7, + hl2: 7, + hh2: 8, + lh1: 8, + hl1: 8, + hh1: 9, + } + } +} + +impl Quant { + const NAME: &'static str = "RfxFrameEnd"; + + const FIXED_PART_SIZE: usize = 5 /* 10 * 4 bits */; +} + +impl Encode for Quant { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let mut level3 = 0; + level3.set_bits(0..4, u16::from(self.ll3)); + level3.set_bits(4..8, u16::from(self.lh3)); + level3.set_bits(8..12, u16::from(self.hl3)); + level3.set_bits(12..16, u16::from(self.hh3)); + + let mut level2_with_lh1 = 0; + level2_with_lh1.set_bits(0..4, u16::from(self.lh2)); + level2_with_lh1.set_bits(4..8, u16::from(self.hl2)); + level2_with_lh1.set_bits(8..12, u16::from(self.hh2)); + level2_with_lh1.set_bits(12..16, u16::from(self.lh1)); + + let mut level1 = 0; + level1.set_bits(0..4, self.hl1); + level1.set_bits(4..8, self.hh1); + + dst.write_u16(level3); + dst.write_u16(level2_with_lh1); + dst.write_u8(level1); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Quant { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + #![allow( + clippy::similar_names, + reason = "it’s hard to do better than ll3, lh3, etc without going overly verbose" + )] + + ensure_fixed_part_size!(in: src); + + let ll3lh3 = src.read_u8(); + let ll3 = ll3lh3.get_bits(0..4); + let lh3 = ll3lh3.get_bits(4..8); + + let hl3hh3 = src.read_u8(); + let hl3 = hl3hh3.get_bits(0..4); + let hh3 = hl3hh3.get_bits(4..8); + + let lh2hl2 = src.read_u8(); + let lh2 = lh2hl2.get_bits(0..4); + let hl2 = lh2hl2.get_bits(4..8); + + let hh2lh1 = src.read_u8(); + let hh2 = hh2lh1.get_bits(0..4); + let lh1 = hh2lh1.get_bits(4..8); + + let hl1hh1 = src.read_u8(); + let hl1 = hl1hh1.get_bits(0..4); + let hh1 = hl1hh1.get_bits(4..8); + + Ok(Self { + ll3, + lh3, + hl3, + hh3, + lh2, + hl2, + hh2, + lh1, + hl1, + hh1, + }) + } +} +/// [2.2.2.3.4.1] TS_RFX_TILE +/// +/// [2.2.2.3.4.1]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/89e669ed-b6dd-4591-a267-73a72bc6d84e +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Tile<'a> { + pub y_quant_index: u8, + pub cb_quant_index: u8, + pub cr_quant_index: u8, + + pub x: u16, + pub y: u16, + + pub y_data: &'a [u8], + pub cb_data: &'a [u8], + pub cr_data: &'a [u8], +} + +impl Tile<'_> { + const NAME: &'static str = "RfxTile"; + + const FIXED_PART_SIZE: usize = 1 /* quantIdxY */ + 1 /* quantIdxCb */ + 1 /* quantIdxCr */ + 2 /* xIdx */ + 2 /* yIdx */ + 2 /* YLen */ + 2 /* CbLen */ + 2 /* CrLen */; +} + +impl Encode for Tile<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u8(self.y_quant_index); + dst.write_u8(self.cb_quant_index); + dst.write_u8(self.cr_quant_index); + + dst.write_u16(self.x); + dst.write_u16(self.y); + + dst.write_u16(cast_length!("YLen", self.y_data.len())?); + dst.write_u16(cast_length!("CbLen", self.cb_data.len())?); + dst.write_u16(cast_length!("CrLen", self.cr_data.len())?); + + dst.write_slice(self.y_data); + dst.write_slice(self.cb_data); + dst.write_slice(self.cr_data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.y_data.len() + self.cb_data.len() + self.cr_data.len() + } +} + +impl<'de> Decode<'de> for Tile<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + #![allow(clippy::similar_names)] // It’s hard to find better names for cr, cb, etc. + ensure_fixed_part_size!(in: src); + + let y_quant_index = src.read_u8(); + let cb_quant_index = src.read_u8(); + let cr_quant_index = src.read_u8(); + + let x = src.read_u16(); + let y = src.read_u16(); + + let y_component_length = usize::from(src.read_u16()); + let cb_component_length = usize::from(src.read_u16()); + let cr_component_length = usize::from(src.read_u16()); + + ensure_size!(in: src, size: y_component_length + cb_component_length + cr_component_length); + + let y_data = src.read_slice(y_component_length); + let cb_data = src.read_slice(cb_component_length); + let cr_data = src.read_slice(cr_component_length); + + Ok(Self { + y_quant_index, + cb_quant_index, + cr_quant_index, + + x, + y, + + y_data, + cb_data, + cr_data, + }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(u16)] +pub enum EntropyAlgorithm { + Rlgr1 = 0x01, + Rlgr3 = 0x04, +} + +impl EntropyAlgorithm { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct OperatingMode: u16 { + const IMAGE_MODE = 0x02; // if not set, the codec is operating in video mode + } +} diff --git a/crates/ironrdp-pdu/src/codecs/rfx/header_messages.rs b/crates/ironrdp-pdu/src/codecs/rfx/header_messages.rs new file mode 100644 index 00000000..a2d22d3c --- /dev/null +++ b/crates/ironrdp-pdu/src/codecs/rfx/header_messages.rs @@ -0,0 +1,250 @@ +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +const SYNC_MAGIC: u32 = 0xCACC_ACCA; +const SYNC_VERSION: u16 = 0x0100; +const CODECS_NUMBER: u8 = 1; +const CODEC_ID: u8 = 1; +const CODEC_VERSION: u16 = 0x0100; +const CHANNEL_ID: u8 = 0; + +// [2.2.2.2.1] TS_RFX_SYNC +// +// [2.2.2.2.1]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/f01b81b6-1a8f-49fd-9543-081fbc8e1831 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SyncPdu; + +impl SyncPdu { + const NAME: &'static str = "RfxSync"; + + const FIXED_PART_SIZE: usize = 4 /* magic */ + 2 /* version */; +} + +impl Encode for SyncPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(SYNC_MAGIC); + dst.write_u16(SYNC_VERSION); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for SyncPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let magic = src.read_u32(); + if magic != SYNC_MAGIC { + return Err(invalid_field_err!("magic", "Invalid sync magic")); + } + let version = src.read_u16(); + if version != SYNC_VERSION { + return Err(invalid_field_err!("version", "Invalid sync version")); + } + + Ok(Self) + } +} + +/// [2.2.2.2.2] TS_RFX_CODEC_VERSIONS +/// +/// [2.2.2.2.2]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/2650e6c2-faf7-4858-b169-828db842b663 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CodecVersionsPdu; + +impl CodecVersionsPdu { + const NAME: &'static str = "RfxCodecVersions"; + + const FIXED_PART_SIZE: usize = 1 /* numCodecs */ + CodecVersion::FIXED_PART_SIZE /* codecs */; +} + +impl Encode for CodecVersionsPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u8(CODECS_NUMBER); + CodecVersion.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for CodecVersionsPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let codec_number = src.read_u8(); + if codec_number != CODECS_NUMBER { + return Err(invalid_field_err!("codec_number", "Invalid codec number")); + } + + let _codec_version = CodecVersion::decode(src)?; + + Ok(Self) + } +} + +/// [2.2.2.2.3] TS_RFX_CHANNELS +/// +/// [2.2.2.2.3]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/c6efba0b-f59e-4d8e-8d76-840c41edce5b +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChannelsPdu(pub Vec); + +impl ChannelsPdu { + const NAME: &'static str = "RfxChannels"; + + const FIXED_PART_SIZE: usize = 1 /* numChannels */; +} + +impl Encode for ChannelsPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u8(cast_length!("num_channels", self.0.len())?); + for channel in &self.0 { + channel.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.0.iter().map(|channel| channel.size()).sum::() + } +} + +impl<'de> Decode<'de> for ChannelsPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let num_channels = usize::from(src.read_u8()); + let channels = core::iter::repeat_with(|| RfxChannel::decode(src)) + .take(num_channels) + .collect::>>()?; + + Ok(Self(channels)) + } +} + +/// [2.2.2.1.3] TS_RFX_CHANNELT +/// +/// [2.2.2.1.3]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/4060f07e-9d73-454d-841e-131a93aca675 +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct RfxChannel { + pub width: i16, + pub height: i16, +} + +impl RfxChannel { + const NAME: &'static str = "RfxChannel"; + + const FIXED_PART_SIZE: usize = 1 /* channelId */ + 2 /* width */ + 2 /* height */; +} + +impl Encode for RfxChannel { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u8(CHANNEL_ID); + dst.write_i16(self.width); + dst.write_i16(self.height); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for RfxChannel { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let channel_id = src.read_u8(); + if channel_id != CHANNEL_ID { + return Err(invalid_field_err!("channelId", "Invalid channel ID")); + } + + let width = src.read_i16(); + let height = src.read_i16(); + + Ok(Self { width, height }) + } +} + +/// [2.2.2.1.4] TS_RFX_CODEC_VERSIONT +/// +/// [2.2.2.1.4] https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/024fee4a-197d-479e-a68f-861933a34b06 +#[derive(Debug, Clone, PartialEq)] +struct CodecVersion; + +impl CodecVersion { + const NAME: &'static str = "RfxCodecVersion"; + + const FIXED_PART_SIZE: usize = 1 /* codecId */ + 2 /* codecVersion */; +} + +impl Encode for CodecVersion { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u8(CODEC_ID); + dst.write_u16(CODEC_VERSION); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for CodecVersion { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let codec_id = src.read_u8(); + if codec_id != CODEC_ID { + return Err(invalid_field_err!("codec_id", "Invalid codec ID")); + } + let codec_version = src.read_u16(); + if codec_version != CODEC_VERSION { + return Err(invalid_field_err!("codec_version", "Invalid codec version")); + } + + Ok(Self) + } +} diff --git a/crates/ironrdp-pdu/src/codecs/rfx/mod.rs b/crates/ironrdp-pdu/src/codecs/rfx/mod.rs new file mode 100644 index 00000000..992cdbf3 --- /dev/null +++ b/crates/ironrdp-pdu/src/codecs/rfx/mod.rs @@ -0,0 +1,341 @@ +mod data_messages; +mod header_messages; + +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use crate::rdp::capability_sets::{RfxCaps, RfxCapset}; + +#[rustfmt::skip] +pub use self::data_messages::{ + ContextPdu, EntropyAlgorithm, FrameBeginPdu, FrameEndPdu, OperatingMode, Quant, RegionPdu, RfxRectangle, Tile, + TileSetPdu, +}; +pub use self::header_messages::{ChannelsPdu, CodecVersionsPdu, RfxChannel, SyncPdu}; + +const CODEC_ID: u8 = 1; +const CHANNEL_ID_FOR_CONTEXT: u8 = 0xFF; +const CHANNEL_ID_FOR_OTHER_VALUES: u8 = 0x00; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Block<'a> { + Tile(Tile<'a>), + Caps(RfxCaps), + CapabilitySet(RfxCapset), + Sync(SyncPdu), + CodecVersions(CodecVersionsPdu), + Channels(ChannelsPdu), + CodecChannel(CodecChannel<'a>), +} + +impl Block<'_> { + const NAME: &'static str = "RfxBlock"; + + const FIXED_PART_SIZE: usize = BlockHeader::FIXED_PART_SIZE; + + pub fn block_type(&self) -> BlockType { + match self { + Block::Tile(_) => BlockType::Tile, + Block::Caps(_) => BlockType::Capabilities, + Block::CapabilitySet(_) => BlockType::CapabilitySet, + Block::Sync(_) => BlockType::Sync, + Block::Channels(_) => BlockType::Channels, + Block::CodecVersions(_) => BlockType::CodecVersions, + Block::CodecChannel(CodecChannel::Context(_)) => BlockType::Context, + Block::CodecChannel(CodecChannel::FrameBegin(_)) => BlockType::FrameBegin, + Block::CodecChannel(CodecChannel::FrameEnd(_)) => BlockType::FrameEnd, + Block::CodecChannel(CodecChannel::Region(_)) => BlockType::Region, + Block::CodecChannel(CodecChannel::TileSet(_)) => BlockType::Extension, + } + } +} + +impl Encode for Block<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let ty = self.block_type(); + let data_length = self.size(); + BlockHeader { ty, data_length }.encode(dst)?; + + if let Block::CodecChannel(ref c) = self { + let channel_id = c.channel_id(); + CodecChannelHeader { channel_id }.encode(dst)?; + } + + match self { + Block::Tile(t) => t.encode(dst), + Block::Caps(c) => c.encode(dst), + Block::CapabilitySet(c) => c.encode(dst), + Block::Sync(s) => s.encode(dst), + Block::Channels(c) => c.encode(dst), + Block::CodecVersions(c) => c.encode(dst), + Block::CodecChannel(CodecChannel::Context(c)) => c.encode(dst), + Block::CodecChannel(CodecChannel::FrameBegin(f)) => f.encode(dst), + Block::CodecChannel(CodecChannel::FrameEnd(f)) => f.encode(dst), + Block::CodecChannel(CodecChannel::Region(r)) => r.encode(dst), + Block::CodecChannel(CodecChannel::TileSet(t)) => t.encode(dst), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + if matches!(self, Block::CodecChannel(_)) { + CodecChannelHeader::FIXED_PART_SIZE + } else { + 0 + } + + match self { + Block::Tile(t) => t.size(), + Block::Caps(c) => c.size(), + Block::CapabilitySet(c) => c.size(), + Block::Sync(s) => s.size(), + Block::Channels(c) => c.size(), + Block::CodecVersions(c) => c.size(), + Block::CodecChannel(CodecChannel::Context(c)) => c.size(), + Block::CodecChannel(CodecChannel::FrameBegin(f)) => f.size(), + Block::CodecChannel(CodecChannel::FrameEnd(f)) => f.size(), + Block::CodecChannel(CodecChannel::Region(r)) => r.size(), + Block::CodecChannel(CodecChannel::TileSet(t)) => t.size(), + } + } +} + +impl<'de> Decode<'de> for Block<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let header = BlockHeader::decode(src)?; + let mut len = header.size(); + if header.ty.is_channel() { + let channel = CodecChannelHeader::decode(src)?; + let expected_id = if header.ty == BlockType::Context { + CHANNEL_ID_FOR_CONTEXT + } else { + CHANNEL_ID_FOR_OTHER_VALUES + }; + if channel.channel_id != expected_id { + return Err(invalid_field_err!("channelId", "Invalid channel ID")); + } + len += channel.size(); + } + let data_len = header + .data_length + .checked_sub(len) + .ok_or_else(|| invalid_field_err!("blockLen", "Invalid block length"))?; + ensure_size!(in: src, size: data_len); + let src = &mut ReadCursor::new(src.read_slice(data_len)); + match header.ty { + BlockType::Tile => Ok(Self::Tile(Tile::decode(src)?)), + BlockType::Capabilities => Ok(Self::Caps(RfxCaps::decode(src)?)), + BlockType::CapabilitySet => Ok(Self::CapabilitySet(RfxCapset::decode(src)?)), + BlockType::Sync => Ok(Self::Sync(SyncPdu::decode(src)?)), + BlockType::Channels => Ok(Self::Channels(ChannelsPdu::decode(src)?)), + BlockType::CodecVersions => Ok(Self::CodecVersions(CodecVersionsPdu::decode(src)?)), + BlockType::Context => Ok(Self::CodecChannel(CodecChannel::Context(ContextPdu::decode(src)?))), + BlockType::FrameBegin => Ok(Self::CodecChannel(CodecChannel::FrameBegin(FrameBeginPdu::decode( + src, + )?))), + BlockType::FrameEnd => Ok(Self::CodecChannel(CodecChannel::FrameEnd(FrameEndPdu::decode(src)?))), + BlockType::Region => Ok(Self::CodecChannel(CodecChannel::Region(RegionPdu::decode(src)?))), + BlockType::Extension => Ok(Self::CodecChannel(CodecChannel::TileSet(TileSetPdu::decode(src)?))), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CodecChannel<'a> { + Context(ContextPdu), + FrameBegin(FrameBeginPdu), + FrameEnd(FrameEndPdu), + Region(RegionPdu), + TileSet(TileSetPdu<'a>), +} + +impl CodecChannel<'_> { + fn channel_id(&self) -> u8 { + if matches!(self, CodecChannel::Context(_)) { + CHANNEL_ID_FOR_CONTEXT + } else { + CHANNEL_ID_FOR_OTHER_VALUES + } + } +} + +/// [2.2.2.1.1] TS_RFX_BLOCKT +/// +/// [2.2.2.1.1]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/1e1b69a9-c2aa-4b13-bd44-23dcf96d4a74 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockHeader { + pub ty: BlockType, + pub data_length: usize, +} + +impl BlockHeader { + const NAME: &'static str = "RfxBlockHeader"; + + const FIXED_PART_SIZE: usize = 2 /* blockType */ + 4 /* blockLen */; +} + +impl Encode for BlockHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.ty.as_u16()); + dst.write_u32(cast_length!("data len", self.data_length)?); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for BlockHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let ty = src.read_u16(); + let ty = BlockType::from_u16(ty).ok_or_else(|| invalid_field_err!("blockType", "Invalid block type"))?; + let data_length: usize = cast_length!("block length", src.read_u32())?; + data_length + .checked_sub(Self::FIXED_PART_SIZE) + .ok_or_else(|| invalid_field_err!("blockLen", "Invalid block length"))?; + + Ok(Self { ty, data_length }) + } +} + +/// [2.2.2.1.2] TS_RFX_CODEC_CHANNELT +/// +/// [2.2.2.1.2]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/56b78b0c-6eef-40cc-b9da-96d21f197c14 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CodecChannelHeader { + channel_id: u8, +} + +impl CodecChannelHeader { + const NAME: &'static str = "CodecChannelHeader"; + + const FIXED_PART_SIZE: usize = 1 /* codecId */ + 1 /* channelId */; +} + +impl Encode for CodecChannelHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u8(CODEC_ID); + dst.write_u8(self.channel_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for CodecChannelHeader { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let codec_id = src.read_u8(); + if codec_id != CODEC_ID { + return Err(invalid_field_err!("codecId", "Invalid codec ID")); + } + + let channel_id = src.read_u8(); + + Ok(Self { channel_id }) + } +} + +/// [2.2.3.1] TS_FRAME_ACKNOWLEDGE_PDU +/// +/// [2.2.3.1]: https://learn.microsoft.com/pt-br/openspecs/windows_protocols/ms-rdprfx/24364aa2-9a7f-4d86-bcfb-67f5a6c19064 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FrameAcknowledgePdu { + pub frame_id: u32, +} + +impl FrameAcknowledgePdu { + const NAME: &'static str = "FrameAcknowledgePdu"; + + const FIXED_PART_SIZE: usize = 4 /* frameId */; +} + +impl Encode for FrameAcknowledgePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.frame_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for FrameAcknowledgePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let frame_id = src.read_u32(); + + Ok(Self { frame_id }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(u16)] +pub enum BlockType { + Tile = 0xCAC3, + Capabilities = 0xCBC0, + CapabilitySet = 0xCBC1, + Sync = 0xCCC0, + CodecVersions = 0xCCC1, + Channels = 0xCCC2, + Context = 0xCCC3, + FrameBegin = 0xCCC4, + FrameEnd = 0xCCC5, + Region = 0xCCC6, + Extension = 0xCCC7, +} + +impl BlockType { + fn is_channel(&self) -> bool { + matches!( + self, + BlockType::Context | BlockType::FrameBegin | BlockType::FrameEnd | BlockType::Region | BlockType::Extension + ) + } + + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} diff --git a/crates/ironrdp-pdu/src/crypto/mod.rs b/crates/ironrdp-pdu/src/crypto/mod.rs new file mode 100644 index 00000000..aa76b1e3 --- /dev/null +++ b/crates/ironrdp-pdu/src/crypto/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod rc4; +pub(crate) mod rsa; diff --git a/crates/ironrdp-pdu/src/crypto/rc4.rs b/crates/ironrdp-pdu/src/crypto/rc4.rs new file mode 100644 index 00000000..abcafcf0 --- /dev/null +++ b/crates/ironrdp-pdu/src/crypto/rc4.rs @@ -0,0 +1,152 @@ +use core::{fmt, ops}; + +#[derive(Debug, Clone)] +pub(crate) struct Rc4 { + i: usize, + j: usize, + state: State, +} + +impl Rc4 { + pub(crate) fn new(key: &[u8]) -> Self { + // key scheduling + let mut state = State::default(); + for (i, item) in (0..=255).zip(state.iter_mut()) { + *item = i; + } + let mut j = 0usize; + for i in 0..256 { + j = (j + usize::from(state[i]) + usize::from(key[i % key.len()])) % 256; + state.swap(i, j); + } + + Self { i: 0, j: 0, state } + } + + pub(crate) fn process(&mut self, message: &[u8]) -> Vec { + // PRGA + let mut output = Vec::with_capacity(message.len()); + while output.capacity() > output.len() { + self.i = (self.i + 1) % 256; + self.j = (self.j + usize::from(self.state[self.i])) % 256; + self.state.swap(self.i, self.j); + let idx_k = (usize::from(self.state[self.i]) + usize::from(self.state[self.j])) % 256; + let k = self.state[idx_k]; + let idx_msg = output.len(); + output.push(k ^ message[idx_msg]); + } + + output + } +} + +#[derive(Clone)] +struct State([u8; 256]); + +impl Default for State { + fn default() -> Self { + Self([0; 256]) + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl ops::Deref for State { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl ops::DerefMut for State { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.as_mut() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn check_common_case() { + let key = "key".to_owned(); + let message = "message".to_owned(); + let expected = [0x66, 0x09, 0x47, 0x9E, 0x45, 0xE8, 0x1E]; + assert_eq!(Rc4::new(key.as_bytes()).process(message.as_bytes())[..], expected); + } + + #[test] + fn one_symbol_key() { + let key = "0".to_owned(); + let message = "message".to_owned(); + let expected = [0xE5, 0x1A, 0xD5, 0xF3, 0xA2, 0x1C, 0xB1]; + assert_eq!(Rc4::new(key.as_bytes()).process(message.as_bytes())[..], expected); + } + + #[test] + fn one_symbol_similar_key_and_message() { + let key = "0".to_owned(); + let message = "0".to_owned(); + let expected = [0xb8]; + assert_eq!(Rc4::new(key.as_bytes()).process(message.as_bytes())[..], expected); + } + + #[test] + fn one_symbol_key_and_message() { + let key = "0".to_owned(); + let message = "a".to_owned(); + let expected = [0xe9]; + assert_eq!(Rc4::new(key.as_bytes()).process(message.as_bytes())[..], expected); + } + + #[test] + fn empty_message() { + let key = "key".to_owned(); + let message = "".to_owned(); + let expected = []; + assert_eq!(Rc4::new(key.as_bytes()).process(message.as_bytes())[..], expected); + } + + #[test] + fn long_key() { + let key = "oigjwr984 874Y8 7W68 8&$y*%&78 4 8724JIOGROGN I4UI928 98FRUWNKRJB GRGg ergeowp".to_owned(); + let message = "message".to_owned(); + let expected = [0xBE, 0x74, 0xEB, 0x88, 0x64, 0x8E, 0x6A]; + assert_eq!(Rc4::new(key.as_bytes()).process(message.as_bytes())[..], expected); + } + + #[test] + fn long_message() { + let key = "key".to_owned(); + let message = "oigjwr984 874Y8 7W68 8&$y*%&78 4 8724JIOGROGN I4UI928 98FRUWNKRJB GRGg ergeowp".to_owned(); + let expected = [ + 0x64, 0x05, 0x53, 0x87, 0x53, 0xFD, 0x42, 0x72, 0x7C, 0x6B, 0x30, 0x4C, 0x22, 0x04, 0x2A, 0xDD, 0xB8, 0x23, + 0xDB, 0x5E, 0x8B, 0xD9, 0xC5, 0xDB, 0x4F, 0xD9, 0x8D, 0x9B, 0x0E, 0xD4, 0x5B, 0xAA, 0x34, 0x1D, 0x8E, 0xB9, + 0x9B, 0xBB, 0xF0, 0xF5, 0x7C, 0x90, 0xAD, 0xFE, 0x64, 0x33, 0x06, 0xCA, 0xCE, 0x68, 0x71, 0x1E, 0x5E, 0xE1, + 0x29, 0xBD, 0xCB, 0x29, 0x6A, 0x6D, 0xD4, 0xC9, 0x99, 0x59, 0xE9, 0x3B, 0xCC, 0x97, 0xEE, 0x32, 0xB5, 0x98, + 0x57, 0x1C, 0x13, 0x6D, 0x35, 0x0C, 0xDE, + ]; + assert_eq!(Rc4::new(key.as_bytes()).process(message.as_bytes())[..], expected[..]); + } + + #[test] + fn long_key_message() { + let key = "iogjerwo ghoreh trojtrj trjrohjigjw9iehgfwe 315 989&*$*%&* &*^*& q 4unkregeor 847847786 ^&**^*" + .to_owned(); + let message = "oigjwr984 874Y8 7W68 8&$y*%&78 4 8724JIOGROGN I4UI928 98FRUWNKRJB GRGg ergeowp".to_owned(); + let expected = [ + 0x6B, 0x92, 0x32, 0x1B, 0xAD, 0x5A, 0x3A, 0x62, 0xE4, 0xC9, 0xD4, 0x2A, 0xAF, 0x34, 0xF1, 0xA3, 0xA0, 0x23, + 0x5B, 0x8D, 0x12, 0x7B, 0x4C, 0xE6, 0x23, 0xE6, 0x13, 0x81, 0xF0, 0xDA, 0xE0, 0x02, 0x65, 0x71, 0x2B, 0x1D, + 0x39, 0x17, 0x2A, 0x7E, 0x60, 0x68, 0x26, 0x2B, 0xF0, 0x46, 0x03, 0xA0, 0x40, 0xC4, 0xBA, 0x78, 0xF9, 0x82, + 0x35, 0x42, 0xE2, 0x8A, 0x69, 0xEE, 0xE0, 0x29, 0x31, 0x66, 0xBE, 0xAF, 0x9E, 0x81, 0xD8, 0x58, 0xCC, 0xA6, + 0x4D, 0xBD, 0xEE, 0x31, 0x32, 0x2A, 0x2F, + ]; + assert_eq!(Rc4::new(key.as_bytes()).process(message.as_bytes())[..], expected[..]); + } +} diff --git a/ironrdp/src/utils/rsa.rs b/crates/ironrdp-pdu/src/crypto/rsa.rs similarity index 51% rename from ironrdp/src/utils/rsa.rs rename to crates/ironrdp-pdu/src/crypto/rsa.rs index 0e2bfea5..d16e1632 100644 --- a/ironrdp/src/utils/rsa.rs +++ b/crates/ironrdp-pdu/src/crypto/rsa.rs @@ -3,54 +3,45 @@ use std::io; use der_parser::parse_der; use num_bigint::BigUint; -pub fn encrypt_with_public_key(message: &[u8], public_key_der: &[u8]) -> io::Result> { - let (_, der_object) = parse_der(&public_key_der).map_err(|err| { +pub(crate) fn encrypt_with_public_key(message: &[u8], public_key_der: &[u8]) -> io::Result> { + let (_, der_object) = parse_der(public_key_der).map_err(|err| { io::Error::new( io::ErrorKind::InvalidData, - format!("Unable to parse public key from der: {:?}", err), + format!("unable to parse public key from DER: {err:?}"), ) })?; let der_object_sequence = der_object.as_sequence().map_err(|err| { io::Error::new( io::ErrorKind::InvalidData, - format!( - "Unable to extract a sequence from the der object. Error: {:?}", - err - ), + format!("unable to extract a sequence from the DER object: {err:?}"), ) })?; if der_object_sequence.len() != 2 { return Err(io::Error::new( io::ErrorKind::InvalidData, - "Der object sequence is empty", + "DER object sequence is empty", )); } let n = der_object_sequence[0].as_slice().map_err(|err| { io::Error::new( io::ErrorKind::InvalidData, - format!( - "Unable to extract a slice from public key modulus sequence: {:?}", - err - ), + format!("unable to extract a slice from public key modulus sequence: {err:?}"), ) })?; let e = der_object_sequence[1].as_slice().map_err(|err| { io::Error::new( io::ErrorKind::InvalidData, - format!( - "Unable to extract a slice from public key exponent sequence: {:?}", - err - ), + format!("unable to extract a slice from public key exponent sequence: {err:?}"), ) })?; - let n = BigUint::from_bytes_be(&n); - let e = BigUint::from_bytes_be(&e); - let m = BigUint::from_bytes_le(&message); + let n = BigUint::from_bytes_be(n); + let e = BigUint::from_bytes_be(e); + let m = BigUint::from_bytes_le(message); let c = m.modpow(&e, &n); let mut result = c.to_bytes_le(); diff --git a/crates/ironrdp-pdu/src/gcc/cluster_data.rs b/crates/ironrdp-pdu/src/gcc/cluster_data.rs new file mode 100644 index 00000000..56753a12 --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/cluster_data.rs @@ -0,0 +1,106 @@ +use std::io; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; +use thiserror::Error; + +const REDIRECTION_VERSION_MASK: u32 = 0x0000_003C; + +const FLAGS_SIZE: usize = 4; +const REDIRECTED_SESSION_ID_SIZE: usize = 4; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientClusterData { + pub flags: RedirectionFlags, + pub redirection_version: RedirectionVersion, + pub redirected_session_id: u32, +} + +impl ClientClusterData { + const NAME: &'static str = "ClientClusterData"; + + const FIXED_PART_SIZE: usize = FLAGS_SIZE + REDIRECTED_SESSION_ID_SIZE; +} + +impl Encode for ClientClusterData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let flags_with_version = self.flags.bits() | (self.redirection_version.as_u32() << 2); + + dst.write_u32(flags_with_version); + dst.write_u32(self.redirected_session_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ClientClusterData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags_with_version = src.read_u32(); + let redirected_session_id = src.read_u32(); + + let flags = RedirectionFlags::from_bits(flags_with_version & !REDIRECTION_VERSION_MASK) + .ok_or_else(|| invalid_field_err!("flags", "invalid redirection flags"))?; + let redirection_version = RedirectionVersion::from_u32((flags_with_version & REDIRECTION_VERSION_MASK) >> 2) + .ok_or_else(|| invalid_field_err!("redirVersion", "invalid redirection version"))?; + + Ok(Self { + flags, + redirection_version, + redirected_session_id, + }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct RedirectionFlags: u32 { + const REDIRECTION_SUPPORTED = 0x0000_0001; + const REDIRECTED_SESSION_FIELD_VALID = 0x0000_0002; + const REDIRECTED_SMARTCARD = 0x0000_0040; + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum RedirectionVersion { + V1 = 0, + V2 = 1, + V3 = 2, + V4 = 3, + V5 = 4, + V6 = 5, +} + +impl RedirectionVersion { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[derive(Debug, Error)] +pub enum ClusterDataError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("invalid redirection flags field")] + InvalidRedirectionFlags, +} diff --git a/crates/ironrdp-pdu/src/gcc/conference_create.rs b/crates/ironrdp-pdu/src/gcc/conference_create.rs new file mode 100644 index 00000000..4d3cce3c --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/conference_create.rs @@ -0,0 +1,375 @@ +use ironrdp_core::{ + cast_length, ensure_size, invalid_field_err, other_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +use super::{ClientGccBlocks, ServerGccBlocks}; +use crate::{mcs, per}; + +const CONFERENCE_REQUEST_OBJECT_ID: [u8; 6] = [0, 0, 20, 124, 0, 1]; +const CONFERENCE_REQUEST_CLIENT_TO_SERVER_H221_NON_STANDARD: &[u8; 4] = b"Duca"; +const CONFERENCE_REQUEST_SERVER_TO_CLIENT_H221_NON_STANDARD: &[u8; 4] = b"McDn"; +const CONFERENCE_REQUEST_U16_MIN: u16 = 1001; + +const CONFERENCE_REQUEST_CONNECT_PDU_SIZE: usize = 12; +const CONFERENCE_RESPONSE_CONNECT_PDU_SIZE: usize = 13; +const OBJECT_IDENTIFIER_KEY: u8 = 0; +const CONNECT_GCC_PDU_CONFERENCE_REQUEST_CHOICE: u8 = 0; +const CONNECT_GCC_PDU_CONFERENCE_RESPONSE_CHOICE: u8 = 0x14; +const CONFERENCE_REQUEST_USER_DATA_SELECTION: u8 = 8; +const USER_DATA_NUMBER_OF_SETS: u8 = 1; +const USER_DATA_H221_NON_STANDARD_CHOICE: u8 = 0xc0; +const CONFERENCE_RESPONSE_TAG: u32 = 1; +const CONFERENCE_RESPONSE_RESULT: u8 = 0; +const H221_NON_STANDARD_MIN_LENGTH: usize = 4; +const CONFERENCE_NAME: &[u8] = b"1"; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ConferenceCreateRequest { + /// INVARIANT: `gcc_blocks.size() <= u16::MAX - CONFERENCE_REQUEST_CONNECT_PDU_SIZE` + gcc_blocks: ClientGccBlocks, +} + +impl ConferenceCreateRequest { + const NAME: &'static str = "ConferenceCreateRequest"; + + pub fn new(gcc_blocks: ClientGccBlocks) -> DecodeResult { + // Ensure the invariant on gcc_blocks.size() is respected. + check_invariant(gcc_blocks.size() <= usize::from(u16::MAX) - CONFERENCE_REQUEST_CONNECT_PDU_SIZE).ok_or_else( + || { + invalid_field_err!( + "gcc_blocks", + "gcc_blocks.size() + CONFERENCE_REQUEST_CONNECT_PDU_SIZE > u16::MAX" + ) + }, + )?; + + Ok(Self { gcc_blocks }) + } + + pub fn gcc_blocks(&self) -> &ClientGccBlocks { + &self.gcc_blocks + } + + pub fn into_gcc_blocks(self) -> ClientGccBlocks { + self.gcc_blocks + } +} + +impl Encode for ConferenceCreateRequest { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in:dst, size: self.size()); + + let gcc_blocks_buffer_length = self.gcc_blocks.size(); + + // ConnectData::Key: select type OBJECT_IDENTIFIER + per::write_choice(dst, OBJECT_IDENTIFIER_KEY); + // ConnectData::Key: value + per::write_object_id(dst, CONFERENCE_REQUEST_OBJECT_ID); + + // ConnectData::connectPDU: length + per::write_length( + dst, + cast_length!( + "gccBlocksLen", + gcc_blocks_buffer_length + CONFERENCE_REQUEST_CONNECT_PDU_SIZE + )?, + ); + // ConnectGCCPDU (CHOICE): Select conferenceCreateRequest (0) of type ConferenceCreateRequest + per::write_choice(dst, CONNECT_GCC_PDU_CONFERENCE_REQUEST_CHOICE); + // ConferenceCreateRequest::Selection: select optional userData from ConferenceCreateRequest + per::write_selection(dst, CONFERENCE_REQUEST_USER_DATA_SELECTION); + // ConferenceCreateRequest::ConferenceName + per::write_numeric_string(dst, CONFERENCE_NAME, 1).map_err(|e| other_err!("confName", source: e))?; + per::write_padding(dst, 1); + // UserData (SET OF SEQUENCE) + // one set of UserData + per::write_number_of_sets(dst, USER_DATA_NUMBER_OF_SETS); + // select h221NonStandard + per::write_choice(dst, USER_DATA_H221_NON_STANDARD_CHOICE); + // h221NonStandard: client-to-server H.221 key, "Duca" + per::write_octet_string( + dst, + CONFERENCE_REQUEST_CLIENT_TO_SERVER_H221_NON_STANDARD, + H221_NON_STANDARD_MIN_LENGTH, + ) + .map_err(|e| other_err!("client-to-server", source: e))?; + // H221NonStandardIdentifier (octet string) + per::write_length(dst, cast_length!("gccBlocksLen", gcc_blocks_buffer_length)?); + self.gcc_blocks.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let gcc_blocks_buffer_length = self.gcc_blocks.size(); + let req_length = u16::try_from(CONFERENCE_REQUEST_CONNECT_PDU_SIZE + gcc_blocks_buffer_length) + .expect("per the invariant on self.gcc_blocks, this cast is infallible"); + let length = u16::try_from(gcc_blocks_buffer_length) + .expect("per the invariant on self.gcc_blocks, this cast is infallible"); + + per::CHOICE_SIZE + + CONFERENCE_REQUEST_OBJECT_ID.len() + + per::sizeof_length(usize::from(req_length)) + + CONFERENCE_REQUEST_CONNECT_PDU_SIZE + + per::sizeof_length(usize::from(length)) + + gcc_blocks_buffer_length + } +} + +impl<'de> Decode<'de> for ConferenceCreateRequest { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + // ConnectData + + // ConnectData::Key: select object (0) of type OBJECT_IDENTIFIER + ensure_size!(in: src, size: per::CHOICE_SIZE); + if per::read_choice(src) != OBJECT_IDENTIFIER_KEY { + return Err(invalid_field_err!("ConnectData::Key", "Got unexpected ConnectData key")); + } + // ConnectData::Key: value (OBJECT_IDENTIFIER) + if per::read_object_id(src).map_err(|e| other_err!("value", source: e))? != CONFERENCE_REQUEST_OBJECT_ID { + return Err(invalid_field_err!( + "ConnectData::Key", + "Got unexpected ConnectData key value" + )); + } + + // ConnectData::connectPDU: length + let _length = per::read_length(src).map_err(|e| other_err!("len", source: e))?; + // ConnectGCCPDU (CHOICE): Select conferenceCreateRequest (0) of type ConferenceCreateRequest + ensure_size!(in: src, size: per::CHOICE_SIZE); + if per::read_choice(src) != CONNECT_GCC_PDU_CONFERENCE_REQUEST_CHOICE { + return Err(invalid_field_err!( + "ConnectData::connectPdu", + "Got invalid ConnectGCCPDU choice (expected ConferenceCreateRequest)" + )); + } + // ConferenceCreateRequest::Selection: select optional userData from ConferenceCreateRequest + ensure_size!(in: src, size: per::CHOICE_SIZE); + if per::read_selection(src) != CONFERENCE_REQUEST_USER_DATA_SELECTION { + return Err(invalid_field_err!( + "ConferenceCreateRequest::Selection", + "Got invalid ConferenceCreateRequest selection (expected UserData)", + )); + } + // ConferenceCreateRequest::ConferenceName + per::read_numeric_string(src, 1).map_err(|e| other_err!("confName", source: e))?; + // padding + per::read_padding(src, 1); + + // UserData (SET OF SEQUENCE) + // one set of UserData + ensure_size!(in: src, size: per::CHOICE_SIZE); + if per::read_number_of_sets(src) != USER_DATA_NUMBER_OF_SETS { + return Err(invalid_field_err!( + "ConferenceCreateRequest", + "Got invalid ConferenceCreateRequest number of sets (expected 1)", + )); + } + // select h221NonStandard + ensure_size!(in: src, size: per::CHOICE_SIZE); + if per::read_choice(src) != USER_DATA_H221_NON_STANDARD_CHOICE { + return Err(invalid_field_err!( + "ConferenceCreateRequest", + "Expected UserData H221NonStandard choice", + )); + } + // h221NonStandard: client-to-server H.221 key, "Duca" + if per::read_octet_string(src, H221_NON_STANDARD_MIN_LENGTH) + .map_err(|e| other_err!("client-to-server", source: e))? + != CONFERENCE_REQUEST_CLIENT_TO_SERVER_H221_NON_STANDARD + { + return Err(invalid_field_err!( + "ConferenceCreateRequest", + "Got invalid H221NonStandard client-to-server key", + )); + } + // H221NonStandardIdentifier (octet string) + let (_gcc_blocks_buffer_length, _) = per::read_length(src).map_err(|e| other_err!("len", source: e))?; + let gcc_blocks = ClientGccBlocks::decode(src)?; + + Self::new(gcc_blocks) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ConferenceCreateResponse { + user_id: u16, + /// INVARIANT: `gcc_blocks.size() <= u16::MAX - CONFERENCE_RESPONSE_CONNECT_PDU_SIZE` + gcc_blocks: ServerGccBlocks, +} + +impl ConferenceCreateResponse { + const NAME: &'static str = "ConferenceCreateResponse"; + + pub fn new(user_id: u16, gcc_blocks: ServerGccBlocks) -> DecodeResult { + // Ensure the invariant on gcc_blocks.size() is respected. + check_invariant(gcc_blocks.size() <= usize::from(u16::MAX) - CONFERENCE_RESPONSE_CONNECT_PDU_SIZE).ok_or_else( + || { + invalid_field_err!( + "gcc_blocks", + "gcc_blocks.size() + CONFERENCE_REQUEST_CONNECT_PDU_SIZE > u16::MAX" + ) + }, + )?; + + Ok(Self { user_id, gcc_blocks }) + } + + pub fn gcc_blocks(&self) -> &ServerGccBlocks { + &self.gcc_blocks + } + + pub fn into_gcc_blocks(self) -> ServerGccBlocks { + self.gcc_blocks + } +} + +impl Encode for ConferenceCreateResponse { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let gcc_blocks_buffer_length = self.gcc_blocks.size(); + + // ConnectData::Key: select type OBJECT_IDENTIFIER + per::write_choice(dst, OBJECT_IDENTIFIER_KEY); + // ConnectData::Key: value + per::write_object_id(dst, CONFERENCE_REQUEST_OBJECT_ID); + + // ConnectData::connectPDU: length (MUST be ignored by the client according to [MS-RDPBCGR]) + per::write_length( + dst, + cast_length!( + "gccBlocksLen", + // FIXME: It seems that the addition of 1 here is a bug. + // The fuzzing is not failing because this length is ignored. + gcc_blocks_buffer_length + CONFERENCE_RESPONSE_CONNECT_PDU_SIZE + 1 + )?, + ); + // ConnectGCCPDU (CHOICE): Select conferenceCreateResponse (1) of type ConferenceCreateResponse + per::write_choice(dst, CONNECT_GCC_PDU_CONFERENCE_RESPONSE_CHOICE); + // ConferenceCreateResponse::nodeID (UserID) + per::write_u16(dst, self.user_id, CONFERENCE_REQUEST_U16_MIN).map_err(|e| other_err!("userId", source: e))?; + // ConferenceCreateResponse::tag (INTEGER) + per::write_u32(dst, CONFERENCE_RESPONSE_TAG); + // ConferenceCreateResponse::result (ENUMERATED) + per::write_enum(dst, CONFERENCE_RESPONSE_RESULT); + per::write_number_of_sets(dst, USER_DATA_NUMBER_OF_SETS); + // select h221NonStandard + per::write_choice(dst, USER_DATA_H221_NON_STANDARD_CHOICE); + // h221NonStandard, server-to-client H.221 key, "McDn" + per::write_octet_string( + dst, + CONFERENCE_REQUEST_SERVER_TO_CLIENT_H221_NON_STANDARD, + H221_NON_STANDARD_MIN_LENGTH, + ) + .map_err(|e| other_err!("server-to-client", source: e))?; + // H221NonStandardIdentifier (octet string) + per::write_length(dst, cast_length!("gccBlocksLen", gcc_blocks_buffer_length)?); + self.gcc_blocks.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let gcc_blocks_buffer_length = self.gcc_blocks.size(); + let req_length = u16::try_from(CONFERENCE_RESPONSE_CONNECT_PDU_SIZE + gcc_blocks_buffer_length) + .expect("per the invariant on self.gcc_blocks, this cast is infallible"); + let length = u16::try_from(gcc_blocks_buffer_length) + .expect("per the invariant on self.gcc_blocks, this cast is infallible"); + + per::CHOICE_SIZE + + CONFERENCE_REQUEST_OBJECT_ID.len() + + per::sizeof_length(usize::from(req_length)) + + CONFERENCE_RESPONSE_CONNECT_PDU_SIZE + + per::sizeof_length(usize::from(length)) + + gcc_blocks_buffer_length + } +} + +impl<'de> Decode<'de> for ConferenceCreateResponse { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + // ConnectData::Key: select type OBJECT_IDENTIFIER + ensure_size!(in: src, size: per::CHOICE_SIZE); + if per::read_choice(src) != OBJECT_IDENTIFIER_KEY { + return Err(invalid_field_err!("ConnectData::Key", "Got unexpected ConnectData key")); + } + // ConnectData::Key: value + if per::read_object_id(src).map_err(|e| other_err!("value", source: e))? != CONFERENCE_REQUEST_OBJECT_ID { + return Err(invalid_field_err!( + "ConnectData::Key", + "Got unexpected ConnectData key value" + )); + }; + // ConnectData::connectPDU: length (MUST be ignored by the client according to [MS-RDPBCGR]) + let _length = per::read_length(src).map_err(|e| other_err!("len", source: e))?; + // ConnectGCCPDU (CHOICE): Select conferenceCreateResponse (1) of type ConferenceCreateResponse + ensure_size!(in: src, size: per::CHOICE_SIZE); + if per::read_choice(src) != CONNECT_GCC_PDU_CONFERENCE_RESPONSE_CHOICE { + return Err(invalid_field_err!( + "ConnectData::connectPdu", + "Got invalid ConnectGCCPDU choice (expected ConferenceCreateResponse)" + )); + } + // ConferenceCreateResponse::nodeID (UserID) + let user_id = per::read_u16(src, CONFERENCE_REQUEST_U16_MIN).map_err(|e| other_err!("userId", source: e))?; + // ConferenceCreateResponse::tag (INTEGER) + if per::read_u32(src).map_err(|e| other_err!("tag", source: e))? != CONFERENCE_RESPONSE_TAG { + return Err(invalid_field_err!( + "ConferenceCreateResponse::tag", + "Got unexpected ConferenceCreateResponse tag", + )); + } + // ConferenceCreateResponse::result (ENUMERATED) + if per::read_enum(src, mcs::RESULT_ENUM_LENGTH).map_err(|e| other_err!("result", source: e))? + != CONFERENCE_RESPONSE_RESULT + { + return Err(invalid_field_err!( + "ConferenceCreateResponse::result", + "Got invalid ConferenceCreateResponse result", + )); + } + ensure_size!(in: src, size: per::CHOICE_SIZE); + if per::read_number_of_sets(src) != USER_DATA_NUMBER_OF_SETS { + return Err(invalid_field_err!( + "ConferenceCreateResponse", + "Got invalid ConferenceCreateResponse number of sets (expected 1)", + )); + } + // select h221NonStandard + ensure_size!(in: src, size: per::CHOICE_SIZE); + if per::read_choice(src) != USER_DATA_H221_NON_STANDARD_CHOICE { + return Err(invalid_field_err!( + "ConferenceCreateResponse", + "Expected UserData H221NonStandard choice", + )); + } + // h221NonStandard, server-to-client H.221 key, "McDn" + if per::read_octet_string(src, H221_NON_STANDARD_MIN_LENGTH) + .map_err(|e| other_err!("server-to-client", source: e))? + != CONFERENCE_REQUEST_SERVER_TO_CLIENT_H221_NON_STANDARD + { + return Err(invalid_field_err!( + "ConferenceCreateResponse", + "Got invalid H221NonStandard server-to-client key", + )); + } + let (_gcc_blocks_buffer_length, _) = per::read_length(src).map_err(|e| other_err!("len", source: e))?; + let gcc_blocks = ServerGccBlocks::decode(src)?; + + Self::new(user_id, gcc_blocks) + } +} + +/// Use this when establishing invariants. +#[inline] +#[must_use] +fn check_invariant(condition: bool) -> Option<()> { + condition.then_some(()) +} diff --git a/crates/ironrdp-pdu/src/gcc/core_data/client.rs b/crates/ironrdp-pdu/src/gcc/core_data/client.rs new file mode 100644 index 00000000..1437eb54 --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/core_data/client.rs @@ -0,0 +1,637 @@ +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, ensure_size, invalid_field_err, write_padding, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; +use tap::Pipe as _; + +use super::{RdpVersion, VERSION_SIZE}; +use crate::nego::SecurityProtocol; +use crate::utils; + +pub const IME_FILE_NAME_SIZE: usize = 64; + +const DESKTOP_WIDTH_SIZE: usize = 2; +const DESKTOP_HEIGHT_SIZE: usize = 2; +const COLOR_DEPTH_SIZE: usize = 2; +const SEC_ACCESS_SEQUENCE_SIZE: usize = 2; +const KEYBOARD_LAYOUT_SIZE: usize = 4; +const CLIENT_BUILD_SIZE: usize = 4; +const CLIENT_NAME_SIZE: usize = 32; +const KEYBOARD_TYPE_SIZE: usize = 4; +const KEYBOARD_SUB_TYPE_SIZE: usize = 4; +const KEYBOARD_FUNCTIONAL_KEYS_COUNT_SIZE: usize = 4; + +const POST_BETA_COLOR_DEPTH_SIZE: usize = 2; +const CLIENT_PRODUCT_ID_SIZE: usize = 2; +const SERIAL_NUMBER_SIZE: usize = 4; +const HIGH_COLOR_DEPTH_SIZE: usize = 2; +const SUPPORTED_COLOR_DEPTHS_SIZE: usize = 2; +const EARLY_CAPABILITY_FLAGS_SIZE: usize = 2; +const DIG_PRODUCT_ID_SIZE: usize = 64; +const CONNECTION_TYPE_SIZE: usize = 1; +const PADDING_SIZE: usize = 1; +const SERVER_SELECTED_PROTOCOL_SIZE: usize = 4; +const DESKTOP_PHYSICAL_WIDTH_SIZE: usize = 4; +const DESKTOP_PHYSICAL_HEIGHT_SIZE: usize = 4; +const DESKTOP_ORIENTATION_SIZE: usize = 2; +const DESKTOP_SCALE_FACTOR_SIZE: usize = 4; +const DEVICE_SCALE_FACTOR_SIZE: usize = 4; + +/// 2.2.1.3.2 Client Core Data (TS_UD_CS_CORE) (required part) +/// +/// [2.2.1.3.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/00f1da4a-ee9c-421a-852f-c19f92343d73 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientCoreData { + pub version: RdpVersion, + pub desktop_width: u16, + pub desktop_height: u16, + /// The requested color depth. Values in this field MUST be ignored if the postBeta2ColorDepth field is present. + pub color_depth: ColorDepth, + pub sec_access_sequence: SecureAccessSequence, + pub keyboard_layout: u32, + pub client_build: u32, + pub client_name: String, + pub keyboard_type: KeyboardType, + pub keyboard_subtype: u32, + pub keyboard_functional_keys_count: u32, + pub ime_file_name: String, + pub optional_data: ClientCoreOptionalData, +} + +impl ClientCoreData { + const NAME: &'static str = "ClientCoreData"; + + const FIXED_PART_SIZE: usize = VERSION_SIZE + + DESKTOP_WIDTH_SIZE + + DESKTOP_HEIGHT_SIZE + + COLOR_DEPTH_SIZE + + SEC_ACCESS_SEQUENCE_SIZE + + KEYBOARD_LAYOUT_SIZE + + CLIENT_BUILD_SIZE + + CLIENT_NAME_SIZE + + KEYBOARD_TYPE_SIZE + + KEYBOARD_SUB_TYPE_SIZE + + KEYBOARD_FUNCTIONAL_KEYS_COUNT_SIZE + + IME_FILE_NAME_SIZE; + + pub fn client_color_depth(&self) -> ClientColorDepth { + if let Some(high_color_depth) = self.optional_data.high_color_depth { + if let Some(early_capability_flags) = self.optional_data.early_capability_flags { + if early_capability_flags.contains(ClientEarlyCapabilityFlags::WANT_32_BPP_SESSION) { + ClientColorDepth::Bpp32 + } else { + From::from(high_color_depth) + } + } else { + From::from(high_color_depth) + } + } else if let Some(post_beta_color_depth) = self.optional_data.post_beta2_color_depth { + From::from(post_beta_color_depth) + } else { + From::from(self.color_depth) + } + } +} + +impl Encode for ClientCoreData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let mut client_name_dst = utils::to_utf16_bytes(self.client_name.as_ref()); + client_name_dst.resize(CLIENT_NAME_SIZE - 2, 0); + let mut ime_file_name_dst = utils::to_utf16_bytes(self.ime_file_name.as_ref()); + ime_file_name_dst.resize(IME_FILE_NAME_SIZE - 2, 0); + + dst.write_u32(self.version.0); + dst.write_u16(self.desktop_width); + dst.write_u16(self.desktop_height); + dst.write_u16(self.color_depth.as_u16()); + dst.write_u16(self.sec_access_sequence.as_u16()); + dst.write_u32(self.keyboard_layout); + dst.write_u32(self.client_build); + dst.write_slice(client_name_dst.as_ref()); + dst.write_u16(0); // client name UTF-16 null terminator + dst.write_u32(self.keyboard_type.as_u32()); + dst.write_u32(self.keyboard_subtype); + dst.write_u32(self.keyboard_functional_keys_count); + dst.write_slice(ime_file_name_dst.as_ref()); + dst.write_u16(0); // ime file name UTF-16 null terminator + + self.optional_data.encode(dst) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.optional_data.size() + } +} + +impl<'de> Decode<'de> for ClientCoreData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version = src.read_u32().pipe(RdpVersion); + let desktop_width = src.read_u16(); + let desktop_height = src.read_u16(); + let color_depth = src + .read_u16() + .pipe(ColorDepth::from_u16) + .ok_or_else(|| invalid_field_err!("colorDepth", "invalid color depth"))?; + let sec_access_sequence = src + .read_u16() + .pipe(SecureAccessSequence::from_u16) + .ok_or_else(|| invalid_field_err!("secAccessSequence", "invalid secure access sequence"))?; + let keyboard_layout = src.read_u32(); + let client_build = src.read_u32(); + + let client_name_buffer = src.read_slice(CLIENT_NAME_SIZE); + let client_name = utils::from_utf16_bytes(client_name_buffer) + .trim_end_matches('\u{0}') + .into(); + + let keyboard_type = src + .read_u32() + .pipe(KeyboardType::from_u32) + .ok_or_else(|| invalid_field_err!("keyboardType", "invalid keyboard type"))?; + let keyboard_subtype = src.read_u32(); + let keyboard_functional_keys_count = src.read_u32(); + + let ime_file_name_buffer = src.read_slice(IME_FILE_NAME_SIZE); + let ime_file_name = utils::from_utf16_bytes(ime_file_name_buffer) + .trim_end_matches('\u{0}') + .into(); + + let optional_data = ClientCoreOptionalData::decode(src)?; + + Ok(Self { + version, + desktop_width, + desktop_height, + color_depth, + sec_access_sequence, + keyboard_layout, + client_build, + client_name, + keyboard_type, + keyboard_subtype, + keyboard_functional_keys_count, + ime_file_name, + optional_data, + }) + } +} + +/// 2.2.1.3.2 Client Core Data (TS_UD_CS_CORE) (optional part) +/// +/// For every field in this structure, the previous fields MUST be present in order to be a valid structure. +/// It is incumbent on the user of this structure to ensure that the structure is valid. +/// +/// [2.2.1.3.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/00f1da4a-ee9c-421a-852f-c19f92343d73 +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ClientCoreOptionalData { + /// The requested color depth. Values in this field MUST be ignored if the highColorDepth field is present. + pub post_beta2_color_depth: Option, + pub client_product_id: Option, + pub serial_number: Option, + /// The requested color depth. + pub high_color_depth: Option, + /// Specifies the high color depths that the client is capable of supporting. + pub supported_color_depths: Option, + pub early_capability_flags: Option, + pub dig_product_id: Option, + pub connection_type: Option, + pub server_selected_protocol: Option, + pub desktop_physical_width: Option, + pub desktop_physical_height: Option, + pub desktop_orientation: Option, + pub desktop_scale_factor: Option, + pub device_scale_factor: Option, +} + +impl ClientCoreOptionalData { + const NAME: &'static str = "ClientCoreOptionalData"; +} + +impl Encode for ClientCoreOptionalData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + if let Some(value) = self.post_beta2_color_depth { + dst.write_u16(value.as_u16()); + } + + if let Some(value) = self.client_product_id { + if self.post_beta2_color_depth.is_none() { + return Err(invalid_field_err!( + "postBeta2ColorDepth", + "postBeta2ColorDepth must be present" + )); + } + dst.write_u16(value); + } + + if let Some(value) = self.serial_number { + if self.client_product_id.is_none() { + return Err(invalid_field_err!("clientProductId", "clientProductId must be present")); + } + dst.write_u32(value); + } + + if let Some(value) = self.high_color_depth { + if self.serial_number.is_none() { + return Err(invalid_field_err!("serialNumber", "serialNumber must be present")); + } + dst.write_u16(value.as_u16()); + } + + if let Some(value) = self.supported_color_depths { + if self.high_color_depth.is_none() { + return Err(invalid_field_err!("highColorDepth", "highColorDepth must be present")); + } + dst.write_u16(value.bits()); + } + + if let Some(value) = self.early_capability_flags { + if self.supported_color_depths.is_none() { + return Err(invalid_field_err!( + "supportedColorDepths", + "supportedColorDepths must be present" + )); + } + dst.write_u16(value.bits()); + } + + if let Some(ref value) = self.dig_product_id { + if self.early_capability_flags.is_none() { + return Err(invalid_field_err!( + "earlyCapabilityFlags", + "earlyCapabilityFlags must be present" + )); + } + let mut dig_product_id_buffer = utils::to_utf16_bytes(value); + dig_product_id_buffer.resize(DIG_PRODUCT_ID_SIZE - 2, 0); + dig_product_id_buffer.extend_from_slice([0; 2].as_ref()); // UTF-16 null terminator + + dst.write_slice(dig_product_id_buffer.as_ref()) + } + + if let Some(value) = self.connection_type { + if self.dig_product_id.is_none() { + return Err(invalid_field_err!("digProductId", "digProductId must be present")); + } + dst.write_u8(value.as_u8()); + write_padding!(dst, 1); + } + + if let Some(value) = self.server_selected_protocol { + if self.connection_type.is_none() { + return Err(invalid_field_err!("connectionType", "connectionType must be present")); + } + dst.write_u32(value.bits()) + } + + if let Some(value) = self.desktop_physical_width { + if self.server_selected_protocol.is_none() { + return Err(invalid_field_err!( + "serverSelectedProtocol", + "serverSelectedProtocol must be present" + )); + } + dst.write_u32(value); + } + + if let Some(value) = self.desktop_physical_height { + if self.desktop_physical_width.is_none() { + return Err(invalid_field_err!( + "desktopPhysicalWidth", + "desktopPhysicalWidth must be present" + )); + } + dst.write_u32(value); + } + + if let Some(value) = self.desktop_orientation { + if self.desktop_physical_height.is_none() { + return Err(invalid_field_err!( + "desktopPhysicalHeight", + "desktopPhysicalHeight must be present" + )); + } + dst.write_u16(value); + } + + if let Some(value) = self.desktop_scale_factor { + if self.desktop_orientation.is_none() { + return Err(invalid_field_err!( + "desktopOrientation", + "desktopOrientation must be present" + )); + } + dst.write_u32(value); + } + + if let Some(value) = self.device_scale_factor { + if self.desktop_scale_factor.is_none() { + return Err(invalid_field_err!( + "desktopScaleFactor", + "desktopScaleFactor must be present" + )); + } + dst.write_u32(value); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let mut size = 0; + + if self.post_beta2_color_depth.is_some() { + size += POST_BETA_COLOR_DEPTH_SIZE; + } + if self.client_product_id.is_some() { + size += CLIENT_PRODUCT_ID_SIZE; + } + if self.serial_number.is_some() { + size += SERIAL_NUMBER_SIZE; + } + if self.high_color_depth.is_some() { + size += HIGH_COLOR_DEPTH_SIZE; + } + if self.supported_color_depths.is_some() { + size += SUPPORTED_COLOR_DEPTHS_SIZE; + } + if self.early_capability_flags.is_some() { + size += EARLY_CAPABILITY_FLAGS_SIZE; + } + if self.dig_product_id.is_some() { + size += DIG_PRODUCT_ID_SIZE; + } + if self.connection_type.is_some() { + size += CONNECTION_TYPE_SIZE + PADDING_SIZE; + } + if self.server_selected_protocol.is_some() { + size += SERVER_SELECTED_PROTOCOL_SIZE; + } + if self.desktop_physical_width.is_some() { + size += DESKTOP_PHYSICAL_WIDTH_SIZE; + } + if self.desktop_physical_height.is_some() { + size += DESKTOP_PHYSICAL_HEIGHT_SIZE; + } + if self.desktop_orientation.is_some() { + size += DESKTOP_ORIENTATION_SIZE; + } + if self.desktop_scale_factor.is_some() { + size += DESKTOP_SCALE_FACTOR_SIZE; + } + if self.device_scale_factor.is_some() { + size += DEVICE_SCALE_FACTOR_SIZE; + } + + size + } +} + +macro_rules! try_or_return { + ($expr:expr, $ret:expr) => { + match $expr { + Ok(v) => v, + Err(_) => return Ok($ret), + } + }; +} + +impl<'de> Decode<'de> for ClientCoreOptionalData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let mut optional_data = Self::default(); + + optional_data.post_beta2_color_depth = Some( + ColorDepth::from_u16(try_or_return!(src.try_read_u16(), optional_data)) + .ok_or_else(|| invalid_field_err!("postBeta2ColorDepth", "invalid color depth"))?, + ); + + optional_data.client_product_id = Some(try_or_return!(src.try_read_u16(), optional_data)); + optional_data.serial_number = Some(try_or_return!(src.try_read_u32(), optional_data)); + + optional_data.high_color_depth = Some( + HighColorDepth::from_u16(try_or_return!(src.try_read_u16(), optional_data)) + .ok_or_else(|| invalid_field_err!("highColorDepth", "invalid color depth"))?, + ); + + optional_data.supported_color_depths = Some( + SupportedColorDepths::from_bits(try_or_return!(src.try_read_u16(), optional_data)) + .ok_or_else(|| invalid_field_err!("supportedColorDepths", "invalid supported color depths"))?, + ); + + optional_data.early_capability_flags = Some( + ClientEarlyCapabilityFlags::from_bits(try_or_return!(src.try_read_u16(), optional_data)) + .ok_or_else(|| invalid_field_err!("earlyCapabilityFlags", "invalid early capability flags"))?, + ); + + if src.len() < DIG_PRODUCT_ID_SIZE { + return Ok(optional_data); + } + + let dig_product_id = src.read_slice(DIG_PRODUCT_ID_SIZE); + optional_data.dig_product_id = Some(utils::from_utf16_bytes(dig_product_id).trim_end_matches('\u{0}').into()); + + optional_data.connection_type = Some( + ConnectionType::from_u8(try_or_return!(src.try_read_u8(), optional_data)) + .ok_or_else(|| invalid_field_err!("connectionType", "invalid connection type"))?, + ); + + try_or_return!(src.try_read_u8(), optional_data); + + optional_data.server_selected_protocol = Some( + SecurityProtocol::from_bits(try_or_return!(src.try_read_u32(), optional_data)) + .ok_or_else(|| invalid_field_err!("serverSelectedProtocol", "invalid security protocol"))?, + ); + + optional_data.desktop_physical_width = Some(try_or_return!(src.try_read_u32(), optional_data)); + // physical height must be present, if the physical width is present + optional_data.desktop_physical_height = Some(src.read_u32()); + + optional_data.desktop_orientation = Some(try_or_return!(src.try_read_u16(), optional_data)); + optional_data.desktop_scale_factor = Some(try_or_return!(src.try_read_u32(), optional_data)); + // device scale factor must be present, if the desktop scale factor is present + optional_data.device_scale_factor = Some(src.read_u32()); + + Ok(optional_data) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ClientColorDepth { + Bpp4, + Bpp8, + Rgb555Bpp16, + Rgb565Bpp16, + Bpp24, + Bpp32, +} + +impl From for ClientColorDepth { + fn from(color_depth: ColorDepth) -> Self { + match color_depth { + ColorDepth::Bpp4 => ClientColorDepth::Bpp4, + ColorDepth::Bpp8 => ClientColorDepth::Bpp8, + ColorDepth::Rgb555Bpp16 => ClientColorDepth::Rgb555Bpp16, + ColorDepth::Rgb565Bpp16 => ClientColorDepth::Rgb565Bpp16, + ColorDepth::Bpp24 => ClientColorDepth::Bpp24, + } + } +} + +impl From for ClientColorDepth { + fn from(color_depth: HighColorDepth) -> Self { + match color_depth { + HighColorDepth::Bpp4 => ClientColorDepth::Bpp4, + HighColorDepth::Bpp8 => ClientColorDepth::Bpp8, + HighColorDepth::Rgb555Bpp16 => ClientColorDepth::Rgb555Bpp16, + HighColorDepth::Rgb565Bpp16 => ClientColorDepth::Rgb565Bpp16, + HighColorDepth::Bpp24 => ClientColorDepth::Bpp24, + } + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum ColorDepth { + Bpp4 = 0xCA00, + Bpp8 = 0xCA01, + Rgb555Bpp16 = 0xCA02, + Rgb565Bpp16 = 0xCA03, + Bpp24 = 0xCA04, +} + +impl ColorDepth { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, FromPrimitive, Eq, Ord, PartialEq, PartialOrd)] +pub enum HighColorDepth { + Bpp4 = 0x0004, + Bpp8 = 0x0008, + Rgb555Bpp16 = 0x000F, + Rgb565Bpp16 = 0x0010, + Bpp24 = 0x0018, +} + +impl HighColorDepth { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum SecureAccessSequence { + Del = 0xAA03, +} + +impl SecureAccessSequence { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum KeyboardType { + IbmPcXt = 1, + OlivettiIco = 2, + IbmPcAt = 3, + IbmEnhanced = 4, + Nokia1050 = 5, + Nokia9140 = 6, + Japanese = 7, +} + +impl KeyboardType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u32(self) -> u32 { + self as u32 + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum ConnectionType { + NotUsed = 0, // not used as ClientEarlyCapabilityFlags::VALID_CONNECTION_TYPE not set + Modem = 1, + BroadbandLow = 2, + Satellite = 3, + BroadbandHigh = 4, + Wan = 5, + Lan = 6, + Autodetect = 7, +} + +impl ConnectionType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct SupportedColorDepths: u16 { + const BPP24 = 1; + const BPP16 = 2; + const BPP15 = 4; + const BPP32 = 8; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ClientEarlyCapabilityFlags: u16 { + const SUPPORT_ERR_INFO_PDU = 0x0001; + const WANT_32_BPP_SESSION = 0x0002; + const SUPPORT_STATUS_INFO_PDU = 0x0004; + const STRONG_ASYMMETRIC_KEYS = 0x0008; + const RELATIVE_MOUSE_INPUT = 0x0010; + const VALID_CONNECTION_TYPE = 0x0020; + const SUPPORT_MONITOR_LAYOUT_PDU = 0x0040; + const SUPPORT_NET_CHAR_AUTODETECT = 0x0080; + const SUPPORT_DYN_VC_GFX_PROTOCOL =0x0100; + const SUPPORT_DYNAMIC_TIME_ZONE = 0x0200; + const SUPPORT_HEART_BEAT_PDU = 0x0400; + const SUPPORT_SKIP_CHANNELJOIN = 0x0800; + // The source may set any bits + const _ = !0; + } +} diff --git a/crates/ironrdp-pdu/src/gcc/core_data/mod.rs b/crates/ironrdp-pdu/src/gcc/core_data/mod.rs new file mode 100644 index 00000000..5e8978be --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/core_data/mod.rs @@ -0,0 +1,78 @@ +pub(crate) mod client; +pub(crate) mod server; + +use std::io; + +use thiserror::Error; + +use crate::PduError; + +const VERSION_SIZE: usize = 4; + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RdpVersion(pub u32); + +impl From for RdpVersion { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(version: RdpVersion) -> Self { + version.0 + } +} + +impl RdpVersion { + pub const V4: Self = Self(0x0008_0001); + pub const V5_PLUS: Self = Self(0x0008_0004); + pub const V10: Self = Self(0x0008_0005); + pub const V10_1: Self = Self(0x0008_0006); + pub const V10_2: Self = Self(0x0008_0007); + pub const V10_3: Self = Self(0x0008_0008); + pub const V10_4: Self = Self(0x0008_0009); + pub const V10_5: Self = Self(0x0008_000A); + pub const V10_6: Self = Self(0x0008_000B); + pub const V10_7: Self = Self(0x0008_000C); + pub const V10_8: Self = Self(0x0008_000D); + pub const V10_9: Self = Self(0x0008_000E); + pub const V10_10: Self = Self(0x0008_000F); + pub const V10_11: Self = Self(0x0008_0010); + pub const V10_12: Self = Self(0x0008_0011); +} + +#[derive(Debug, Error)] +pub enum CoreDataError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("invalid version field")] + InvalidVersion, + #[error("invalid color depth field")] + InvalidColorDepth, + #[error("invalid post beta color depth field")] + InvalidPostBetaColorDepth, + #[error("invalid high color depth field")] + InvalidHighColorDepth, + #[error("invalid supported color depths field")] + InvalidSupportedColorDepths, + #[error("invalid secure access sequence field")] + InvalidSecureAccessSequence, + #[error("invalid keyboard type field")] + InvalidKeyboardType, + #[error("invalid early capability flags field")] + InvalidEarlyCapabilityFlags, + #[error("invalid connection type field")] + InvalidConnectionType, + #[error("invalid server security protocol field")] + InvalidServerSecurityProtocol, + #[error("PDU error: {0}")] + Pdu(PduError), +} + +impl From for CoreDataError { + fn from(e: PduError) -> Self { + Self::Pdu(e) + } +} diff --git a/crates/ironrdp-pdu/src/gcc/core_data/server.rs b/crates/ironrdp-pdu/src/gcc/core_data/server.rs new file mode 100644 index 00000000..676df2e2 --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/core_data/server.rs @@ -0,0 +1,134 @@ +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; +use tap::Pipe as _; + +use super::RdpVersion; +use crate::nego::SecurityProtocol; + +const CLIENT_REQUESTED_PROTOCOL_SIZE: usize = 4; +const EARLY_CAPABILITY_FLAGS_SIZE: usize = 4; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerCoreData { + pub version: RdpVersion, + pub optional_data: ServerCoreOptionalData, +} + +impl ServerCoreData { + const NAME: &'static str = "ServerCoreData"; + + const FIXED_PART_SIZE: usize = 4 /* rdpVersion */; +} + +impl Encode for ServerCoreData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.version.0); + self.optional_data.encode(dst) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.optional_data.size() + } +} + +impl<'de> Decode<'de> for ServerCoreData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version = src.read_u32().pipe(RdpVersion); + let optional_data = ServerCoreOptionalData::decode(src)?; + + Ok(Self { version, optional_data }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ServerCoreOptionalData { + pub client_requested_protocols: Option, + pub early_capability_flags: Option, +} + +impl ServerCoreOptionalData { + const NAME: &'static str = "ServerCoreOptionalData"; +} + +impl Encode for ServerCoreOptionalData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + if let Some(value) = self.client_requested_protocols { + dst.write_u32(value.bits()); + }; + + if let Some(value) = self.early_capability_flags { + dst.write_u32(value.bits()); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let mut size = 0; + + if self.client_requested_protocols.is_some() { + size += CLIENT_REQUESTED_PROTOCOL_SIZE; + } + if self.early_capability_flags.is_some() { + size += EARLY_CAPABILITY_FLAGS_SIZE; + } + + size + } +} + +macro_rules! try_or_return { + ($expr:expr, $ret:expr) => { + match $expr { + Ok(v) => v, + Err(_) => return Ok($ret), + } + }; +} + +impl<'de> Decode<'de> for ServerCoreOptionalData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let mut optional_data = Self::default(); + + optional_data.client_requested_protocols = Some( + SecurityProtocol::from_bits(try_or_return!(src.try_read_u32(), optional_data)) + .ok_or_else(|| invalid_field_err!("clientReqProtocols", "invalid server security protocol"))?, + ); + + optional_data.early_capability_flags = Some( + ServerEarlyCapabilityFlags::from_bits(try_or_return!(src.try_read_u32(), optional_data)) + .ok_or_else(|| invalid_field_err!("earlyCapFlags", "invalid early capability flags"))?, + ); + + Ok(optional_data) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ServerEarlyCapabilityFlags: u32 { + const EDGE_ACTIONS_SUPPORTED_V1 = 0x0000_0001; + const DYNAMIC_DST_SUPPORTED = 0x0000_0002; + const EDGE_ACTIONS_SUPPORTED_V2 = 0x0000_0004; + const SKIP_CHANNELJOIN_SUPPORTED = 0x0000_0008; + // The source may set any bits + const _ = !0; + } +} diff --git a/crates/ironrdp-pdu/src/gcc/message_channel_data.rs b/crates/ironrdp-pdu/src/gcc/message_channel_data.rs new file mode 100644 index 00000000..b01a27c6 --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/message_channel_data.rs @@ -0,0 +1,80 @@ +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +const CLIENT_FLAGS_SIZE: usize = 4; +const SERVER_MCS_MESSAGE_CHANNEL_ID_SIZE: usize = 2; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientMessageChannelData; + +impl ClientMessageChannelData { + const NAME: &'static str = "ClientMessageChannelData"; + + const FIXED_PART_SIZE: usize = CLIENT_FLAGS_SIZE; +} + +impl Encode for ClientMessageChannelData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(0); // flags + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ClientMessageChannelData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _flags = src.read_u32(); // is unused + + Ok(Self) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerMessageChannelData { + pub mcs_message_channel_id: u16, +} + +impl ServerMessageChannelData { + const NAME: &'static str = "ServerMessageChannelData"; + + const FIXED_PART_SIZE: usize = SERVER_MCS_MESSAGE_CHANNEL_ID_SIZE; +} + +impl Encode for ServerMessageChannelData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.mcs_message_channel_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ServerMessageChannelData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let mcs_message_channel_id = src.read_u16(); + + Ok(Self { mcs_message_channel_id }) + } +} diff --git a/crates/ironrdp-pdu/src/gcc/mod.rs b/crates/ironrdp-pdu/src/gcc/mod.rs new file mode 100644 index 00000000..27f787ae --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/mod.rs @@ -0,0 +1,389 @@ +use std::io; + +use ironrdp_core::{ + cast_length, decode, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeErrorKind, DecodeResult, + Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use thiserror::Error; + +use crate::PduError; + +pub mod conference_create; + +mod cluster_data; +mod core_data; +mod message_channel_data; +mod monitor_data; +mod monitor_extended_data; +mod multi_transport_channel_data; +mod network_data; +mod security_data; + +pub use self::cluster_data::{ClientClusterData, ClusterDataError, RedirectionFlags, RedirectionVersion}; +pub use self::conference_create::{ConferenceCreateRequest, ConferenceCreateResponse}; +pub use self::core_data::client::{ + ClientColorDepth, ClientCoreData, ClientCoreOptionalData, ClientEarlyCapabilityFlags, ColorDepth, ConnectionType, + HighColorDepth, KeyboardType, SecureAccessSequence, SupportedColorDepths, IME_FILE_NAME_SIZE, +}; +pub use self::core_data::server::{ServerCoreData, ServerCoreOptionalData, ServerEarlyCapabilityFlags}; +pub use self::core_data::{CoreDataError, RdpVersion}; +pub use self::message_channel_data::{ClientMessageChannelData, ServerMessageChannelData}; +pub use self::monitor_data::{ + ClientMonitorData, Monitor, MonitorFlags, MONITOR_COUNT_SIZE, MONITOR_FLAGS_SIZE, MONITOR_SIZE, +}; +pub use self::monitor_extended_data::{ClientMonitorExtendedData, ExtendedMonitorInfo, MonitorOrientation}; +pub use self::multi_transport_channel_data::{MultiTransportChannelData, MultiTransportFlags}; +pub use self::network_data::{ + ChannelDef, ChannelName, ChannelOptions, ClientNetworkData, NetworkDataError, ServerNetworkData, +}; +pub use self::security_data::{ + ClientSecurityData, EncryptionLevel, EncryptionMethod, SecurityDataError, ServerSecurityData, +}; + +macro_rules! user_header_try { + ($e:expr) => { + match $e { + Ok(user_header) => user_header, + Err(e) if matches!(e.kind(), DecodeErrorKind::NotEnoughBytes { .. }) => break, + Err(e) => return Err(e), + } + }; +} + +const USER_DATA_HEADER_SIZE: usize = 4; + +/// 2.2.1.3 Client MCS Connect Initial PDU with GCC Conference Create Request +/// +/// [2.2.1.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/db6713ee-1c0e-4064-a3b3-0fac30b4037b +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientGccBlocks { + pub core: ClientCoreData, + pub security: ClientSecurityData, + /// According to [MSDN](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/c1bea8bd-069c-4437-9769-db5d27935225), + /// the Client GCC blocks MUST contain Core, Security, Network GCC blocks. + /// But the FreeRDP does not send the Network GCC block if it does not have channels to join, + /// and what is surprising - Windows RDP server accepts this GCC block. + /// Because of this, the Network GCC block is made optional in IronRDP. + pub network: Option, + pub cluster: Option, + pub monitor: Option, + pub message_channel: Option, + pub multi_transport_channel: Option, + pub monitor_extended: Option, +} + +impl ClientGccBlocks { + const NAME: &'static str = "ClientGccBlocks"; + + pub fn channel_names(&self) -> Option> { + self.network.as_ref().map(|network| network.channels.clone()) + } +} + +impl Encode for ClientGccBlocks { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + UserDataHeader::encode(dst, ClientGccType::CoreData.as_u16(), &self.core)?; + UserDataHeader::encode(dst, ClientGccType::SecurityData.as_u16(), &self.security)?; + + if let Some(ref network) = self.network { + UserDataHeader::encode(dst, ClientGccType::NetworkData.as_u16(), network)?; + } + if let Some(ref cluster) = self.cluster { + UserDataHeader::encode(dst, ClientGccType::ClusterData.as_u16(), cluster)?; + } + if let Some(ref monitor) = self.monitor { + UserDataHeader::encode(dst, ClientGccType::MonitorData.as_u16(), monitor)?; + } + if let Some(ref message_channel) = self.message_channel { + UserDataHeader::encode(dst, ClientGccType::MessageChannelData.as_u16(), message_channel)?; + } + if let Some(ref multi_transport_channel) = self.multi_transport_channel { + UserDataHeader::encode( + dst, + ClientGccType::MultiTransportChannelData.as_u16(), + multi_transport_channel, + )?; + } + if let Some(ref monitor_extended) = self.monitor_extended { + UserDataHeader::encode(dst, ClientGccType::MonitorExtendedData.as_u16(), monitor_extended)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let mut size = self.core.size() + self.security.size() + USER_DATA_HEADER_SIZE * 2; + + if let Some(ref network) = self.network { + size += network.size() + USER_DATA_HEADER_SIZE; + } + if let Some(ref cluster) = self.cluster { + size += cluster.size() + USER_DATA_HEADER_SIZE; + } + if let Some(ref monitor) = self.monitor { + size += monitor.size() + USER_DATA_HEADER_SIZE; + } + if let Some(ref message_channel) = self.message_channel { + size += message_channel.size() + USER_DATA_HEADER_SIZE; + } + if let Some(ref multi_transport_channel) = self.multi_transport_channel { + size += multi_transport_channel.size() + USER_DATA_HEADER_SIZE; + } + if let Some(ref monitor_extended) = self.monitor_extended { + size += monitor_extended.size() + USER_DATA_HEADER_SIZE; + } + + size + } +} + +impl<'de> Decode<'de> for ClientGccBlocks { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let mut core = None; + let mut security = None; + let mut network = None; + let mut cluster = None; + let mut monitor = None; + let mut message_channel = None; + let mut multi_transport_channel = None; + let mut monitor_extended = None; + + loop { + let (ty, cur) = user_header_try!(UserDataHeader::decode(src)); + + match ty { + ClientGccType::CoreData => core = Some(decode(cur)?), + ClientGccType::SecurityData => security = Some(decode(cur)?), + ClientGccType::NetworkData => network = Some(decode(cur)?), + ClientGccType::ClusterData => cluster = Some(decode(cur)?), + ClientGccType::MonitorData => monitor = Some(decode(cur)?), + ClientGccType::MessageChannelData => message_channel = Some(decode(cur)?), + ClientGccType::MonitorExtendedData => monitor_extended = Some(decode(cur)?), + ClientGccType::MultiTransportChannelData => multi_transport_channel = Some(decode(cur)?), + }; + } + + Ok(Self { + core: core.ok_or_else(|| invalid_field_err!("core", "required GCC core is absent"))?, + security: security.ok_or_else(|| invalid_field_err!("security", "required GCC security is absent"))?, + network, + cluster, + monitor, + message_channel, + multi_transport_channel, + monitor_extended, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerGccBlocks { + pub core: ServerCoreData, + pub network: ServerNetworkData, + pub security: ServerSecurityData, + pub message_channel: Option, + pub multi_transport_channel: Option, +} + +impl ServerGccBlocks { + const NAME: &'static str = "ServerGccBlocks"; + + pub fn channel_ids(&self) -> Vec { + self.network.channel_ids.clone() + } + pub fn global_channel_id(&self) -> u16 { + self.network.io_channel + } +} + +impl Encode for ServerGccBlocks { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + UserDataHeader::encode(dst, ServerGccType::CoreData.as_u16(), &self.core)?; + UserDataHeader::encode(dst, ServerGccType::NetworkData.as_u16(), &self.network)?; + UserDataHeader::encode(dst, ServerGccType::SecurityData.as_u16(), &self.security)?; + + if let Some(ref message_channel) = self.message_channel { + UserDataHeader::encode(dst, ServerGccType::MessageChannelData.as_u16(), message_channel)?; + } + if let Some(ref multi_transport_channel) = self.multi_transport_channel { + UserDataHeader::encode( + dst, + ServerGccType::MultiTransportChannelData.as_u16(), + multi_transport_channel, + )?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let mut size = self.core.size() + self.network.size() + self.security.size() + USER_DATA_HEADER_SIZE * 3; + + if let Some(ref message_channel) = self.message_channel { + size += message_channel.size() + USER_DATA_HEADER_SIZE; + } + if let Some(ref multi_transport_channel) = self.multi_transport_channel { + size += multi_transport_channel.size() + USER_DATA_HEADER_SIZE; + } + + size + } +} + +impl<'de> Decode<'de> for ServerGccBlocks { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let mut core = None; + let mut network = None; + let mut security = None; + let mut message_channel = None; + let mut multi_transport_channel = None; + + loop { + let (ty, cur) = user_header_try!(UserDataHeader::decode(src)); + + match ty { + ServerGccType::CoreData => core = Some(decode(cur)?), + ServerGccType::NetworkData => network = Some(decode(cur)?), + ServerGccType::SecurityData => security = Some(decode(cur)?), + ServerGccType::MessageChannelData => message_channel = Some(decode(cur)?), + ServerGccType::MultiTransportChannelData => multi_transport_channel = Some(decode(cur)?), + }; + } + + Ok(Self { + core: core.ok_or_else(|| invalid_field_err!("core", "required GCC core is absent"))?, + network: network.ok_or_else(|| invalid_field_err!("network", "required GCC network is absent"))?, + security: security.ok_or_else(|| invalid_field_err!("security", "required GCC security is absent"))?, + message_channel, + multi_transport_channel, + }) + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, FromPrimitive)] +pub enum ClientGccType { + CoreData = 0xC001, + SecurityData = 0xC002, + NetworkData = 0xC003, + ClusterData = 0xC004, + MonitorData = 0xC005, + MessageChannelData = 0xC006, + MonitorExtendedData = 0xC008, + MultiTransportChannelData = 0xC00A, +} + +impl ClientGccType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u16(self) -> u16 { + self as u16 + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, FromPrimitive)] +pub enum ServerGccType { + CoreData = 0x0C01, + SecurityData = 0x0C02, + NetworkData = 0x0C03, + MessageChannelData = 0x0C04, + MultiTransportChannelData = 0x0C08, +} + +impl ServerGccType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u16(self) -> u16 { + self as u16 + } +} + +#[derive(Debug)] +pub struct UserDataHeader; + +impl UserDataHeader { + const FIXED_PART_SIZE: usize = 2 /* blockType */ + 2 /* blockLen */; + + pub fn encode(dst: &mut WriteCursor<'_>, block_type: T, block: &B) -> EncodeResult<()> + where + T: Into, + B: Encode, + { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(block_type.into()); + dst.write_u16(cast_length!("blockLen", block.size() + USER_DATA_HEADER_SIZE)?); + block.encode(dst)?; + + Ok(()) + } + + pub fn decode<'de, T>(src: &mut ReadCursor<'de>) -> DecodeResult<(T, &'de [u8])> + where + T: FromPrimitive, + { + ensure_fixed_part_size!(in: src); + + let block_type = + T::from_u16(src.read_u16()).ok_or_else(|| invalid_field_err!("blockType", "invalid GCC type"))?; + let block_length: usize = cast_length!("blockLen", src.read_u16())?; + + if block_length <= USER_DATA_HEADER_SIZE { + return Err(invalid_field_err!("blockLen", "invalid UserDataHeader length")); + } + + let len = block_length - USER_DATA_HEADER_SIZE; + ensure_size!(in: src, size: len); + + Ok((block_type, src.read_slice(len))) + } +} + +#[derive(Debug, Error)] +pub enum GccError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("core data block error")] + CoreError(#[from] CoreDataError), + #[error("security data block error")] + SecurityError(#[from] SecurityDataError), + #[error("network data block error")] + NetworkError(#[from] NetworkDataError), + #[error("cluster data block error")] + ClusterError(#[from] ClusterDataError), + #[error("invalid GCC block type")] + InvalidGccType, + #[error("invalid conference create request: {0}")] + InvalidConferenceCreateRequest(String), + #[error("invalid Conference create response: {0}")] + InvalidConferenceCreateResponse(String), + #[error("a server did not send the required GCC data block: {0:?}")] + RequiredClientDataBlockIsAbsent(ClientGccType), + #[error("a client did not send the required GCC data block: {0:?}")] + RequiredServerDataBlockIsAbsent(ServerGccType), + #[error("PDU error: {0}")] + Pdu(PduError), +} + +impl From for GccError { + fn from(e: PduError) -> Self { + Self::Pdu(e) + } +} diff --git a/crates/ironrdp-pdu/src/gcc/monitor_data.rs b/crates/ironrdp-pdu/src/gcc/monitor_data.rs new file mode 100644 index 00000000..643d3421 --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/monitor_data.rs @@ -0,0 +1,130 @@ +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +pub const MONITOR_COUNT_SIZE: usize = 4; +pub const MONITOR_SIZE: usize = 20; +pub const MONITOR_FLAGS_SIZE: usize = 4; + +const MONITOR_COUNT_MAX: usize = 16; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientMonitorData { + pub monitors: Vec, +} + +impl ClientMonitorData { + const NAME: &'static str = "ClientMonitorData"; + + const FIXED_PART_SIZE: usize = 4 /* flags */ + 4 /* count */; +} + +impl Encode for ClientMonitorData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(0); // flags + dst.write_u32(cast_length!("nMonitors", self.monitors.len())?); + + for monitor in self.monitors.iter().take(MONITOR_COUNT_MAX) { + monitor.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.monitors.len() * Monitor::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ClientMonitorData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _flags = src.read_u32(); // is unused + let monitor_count = cast_length!("number of monitors", src.read_u32())?; + + if monitor_count > MONITOR_COUNT_MAX { + return Err(invalid_field_err!("nMonitors", "too many monitors")); + } + + let mut monitors = Vec::with_capacity(monitor_count); + for _ in 0..monitor_count { + monitors.push(Monitor::decode(src)?); + } + + Ok(Self { monitors }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Monitor { + pub left: i32, + pub top: i32, + pub right: i32, + pub bottom: i32, + pub flags: MonitorFlags, +} + +impl Monitor { + const NAME: &'static str = "Monitor"; + + const FIXED_PART_SIZE: usize = 4 /* left */ + 4 /* top */ + 4 /* right */ + 4 /* bottom */ + 4 /* flags */; +} + +impl Encode for Monitor { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_i32(self.left); + dst.write_i32(self.top); + dst.write_i32(self.right); + dst.write_i32(self.bottom); + dst.write_u32(self.flags.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Monitor { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let left = src.read_i32(); + let top = src.read_i32(); + let right = src.read_i32(); + let bottom = src.read_i32(); + let flags = MonitorFlags::from_bits(src.read_u32()) + .ok_or_else(|| invalid_field_err!("flags", "invalid monitor flags"))?; + + Ok(Self { + left, + top, + right, + bottom, + flags, + }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct MonitorFlags: u32 { + const PRIMARY = 1; + } +} diff --git a/crates/ironrdp-pdu/src/gcc/monitor_extended_data.rs b/crates/ironrdp-pdu/src/gcc/monitor_extended_data.rs new file mode 100644 index 00000000..334c327c --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/monitor_extended_data.rs @@ -0,0 +1,152 @@ +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +const MONITOR_COUNT_MAX: usize = 16; +const MONITOR_ATTRIBUTE_SIZE: u32 = 20; + +const FLAGS_SIZE: usize = 4; +const MONITOR_ATTRIBUTE_SIZE_FIELD_SIZE: usize = 4; +const MONITOR_COUNT: usize = 4; +const MONITOR_SIZE: usize = 20; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientMonitorExtendedData { + pub extended_monitors_info: Vec, +} + +impl ClientMonitorExtendedData { + const NAME: &'static str = "ClientMonitorExtendedData"; + + const FIXED_PART_SIZE: usize = FLAGS_SIZE + MONITOR_ATTRIBUTE_SIZE_FIELD_SIZE + MONITOR_COUNT; +} + +impl Encode for ClientMonitorExtendedData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(0); // flags + dst.write_u32(MONITOR_ATTRIBUTE_SIZE); // flags + dst.write_u32(cast_length!("nMonitors", self.extended_monitors_info.len())?); + + for extended_monitor_info in self.extended_monitors_info.iter().take(MONITOR_COUNT_MAX) { + extended_monitor_info.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.extended_monitors_info.len() * MONITOR_SIZE + } +} + +impl<'de> Decode<'de> for ClientMonitorExtendedData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _flags = src.read_u32(); // is unused + + let monitor_attribute_size = src.read_u32(); + if monitor_attribute_size != MONITOR_ATTRIBUTE_SIZE { + return Err(invalid_field_err!("monitorAttributeSize", "invalid size")); + } + + let monitor_count = cast_length!("monitorCount", src.read_u32())?; + + if monitor_count > MONITOR_COUNT_MAX { + return Err(invalid_field_err!("monitorCount", "invalid monitor count")); + } + + let mut extended_monitors_info = Vec::with_capacity(monitor_count); + for _ in 0..monitor_count { + extended_monitors_info.push(ExtendedMonitorInfo::decode(src)?); + } + + Ok(Self { extended_monitors_info }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtendedMonitorInfo { + pub physical_width: u32, + pub physical_height: u32, + pub orientation: MonitorOrientation, + pub desktop_scale_factor: u32, + pub device_scale_factor: u32, +} + +impl ExtendedMonitorInfo { + const NAME: &'static str = "ExtendedMonitorInfo"; + + const FIXED_PART_SIZE: usize = MONITOR_SIZE; +} + +impl Encode for ExtendedMonitorInfo { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.physical_width); + dst.write_u32(self.physical_height); + dst.write_u32(u32::from(self.orientation.as_u16())); + dst.write_u32(self.desktop_scale_factor); + dst.write_u32(self.device_scale_factor); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ExtendedMonitorInfo { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let physical_width = src.read_u32(); + let physical_height = src.read_u32(); + let orientation = MonitorOrientation::from_u32(src.read_u32()) + .ok_or_else(|| invalid_field_err!("orientation", "invalid monitor orientation"))?; + let desktop_scale_factor = src.read_u32(); + let device_scale_factor = src.read_u32(); + + Ok(Self { + physical_width, + physical_height, + orientation, + desktop_scale_factor, + device_scale_factor, + }) + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum MonitorOrientation { + Landscape = 0, + Portrait = 90, + LandscapeFlipped = 180, + PortraitFlipped = 270, +} + +impl MonitorOrientation { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u16(self) -> u16 { + self as u16 + } +} diff --git a/crates/ironrdp-pdu/src/gcc/multi_transport_channel_data.rs b/crates/ironrdp-pdu/src/gcc/multi_transport_channel_data.rs new file mode 100644 index 00000000..e9bd3291 --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/multi_transport_channel_data.rs @@ -0,0 +1,54 @@ +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MultiTransportChannelData { + pub flags: MultiTransportFlags, +} + +impl MultiTransportChannelData { + const NAME: &'static str = "MultiTransportChannelData"; + + const FIXED_PART_SIZE: usize = 4 /* flags */; +} + +impl Encode for MultiTransportChannelData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.flags.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for MultiTransportChannelData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = MultiTransportFlags::from_bits(src.read_u32()) + .ok_or_else(|| invalid_field_err!("flags", "invalid multitransport flags"))?; + + Ok(Self { flags }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct MultiTransportFlags: u32 { + const TRANSPORT_TYPE_UDP_FECR = 0x01; + const TRANSPORT_TYPE_UDP_FECL = 0x04; + const TRANSPORT_TYPE_UDP_PREFERRED = 0x100; + const SOFT_SYNC_TCP_TO_UDP = 0x200; + } +} diff --git a/crates/ironrdp-pdu/src/gcc/network_data.rs b/crates/ironrdp-pdu/src/gcc/network_data.rs new file mode 100644 index 00000000..2ea457e1 --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/network_data.rs @@ -0,0 +1,302 @@ +use std::borrow::Cow; +use std::{io, str}; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, write_padding, Decode, + DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_integer::Integer as _; +use thiserror::Error; + +const CHANNELS_MAX: usize = 31; + +const CLIENT_CHANNEL_OPTIONS_SIZE: usize = 4; +const CLIENT_CHANNEL_SIZE: usize = ChannelName::SIZE + CLIENT_CHANNEL_OPTIONS_SIZE; + +const SERVER_IO_CHANNEL_SIZE: usize = 2; +const SERVER_CHANNEL_COUNT_SIZE: usize = 2; +const SERVER_CHANNEL_SIZE: usize = 2; + +/// An 8-byte array containing a null-terminated collection of seven ANSI characters +/// with the purpose of uniquely identifying a channel. +/// +/// In RDP, an ANSI character is a 8-bit Windows-1252 character set unit. ANSI character set +/// is using all the code values from 0 to 255, as such any u8 value is a valid ANSI character. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ChannelName { + /// INVARIANT: A null-terminated 8-byte array. + /// INVARIANT: Contains at most seven ANSI characters. + inner: Cow<'static, [u8; Self::SIZE]>, +} + +impl ChannelName { + pub const SIZE: usize = 8; + + /// Creates a channel name using the provided array, ensuring the last byte is always the null terminator. + pub const fn new(mut value: [u8; Self::SIZE]) -> Self { + value[Self::SIZE - 1] = 0; // ensure the last byte is always the null terminator + + Self { + inner: Cow::Owned(value), + } + } + + /// Converts an UTF-8 string into a channel name by copying up to 7 bytes. + pub fn from_utf8(value: &str) -> Option { + let mut inner = [0; Self::SIZE]; + + value + .chars() + .take(Self::SIZE - 1) + .zip(inner.iter_mut()) + .try_for_each(|(src, dst)| { + let c = u8::try_from(src).ok()?; + c.is_ascii().then(|| *dst = c) + })?; + + Some(Self { + inner: Cow::Owned(inner), + }) + } + + /// Converts a static u8 array into a channel name without copy. + /// + /// # Panics + /// + /// Panics if input is not null-terminated. + pub const fn from_static(value: &'static [u8; 8]) -> Self { + // ensure the last byte is always the null terminator + if value[Self::SIZE - 1] != 0 { + panic!("channel name must be null-terminated") + } + + Self { + inner: Cow::Borrowed(value), + } + } + + /// Returns the underlying raw representation of the channel name (an 8-byte array). + pub fn as_bytes(&self) -> &[u8; Self::SIZE] { + self.inner.as_ref() + } + + pub fn as_str(&self) -> Option<&str> { + if self.inner.iter().all(u8::is_ascii) { + #[expect(clippy::missing_panics_doc, reason = "never panics per invariant on self.inner")] + let terminator_idx = self + .inner + .iter() + .position(|c| *c == 0) + .expect("null-terminated ASCII string"); + + #[expect(clippy::missing_panics_doc, reason = "never panics per invariant on self.inner")] + Some(str::from_utf8(&self.inner[..terminator_idx]).expect("ASCII characters")) + } else { + None + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientNetworkData { + pub channels: Vec, +} + +impl ClientNetworkData { + const NAME: &'static str = "ClientNetworkData"; + + const FIXED_PART_SIZE: usize = 4 /* channelCount */; +} + +impl Encode for ClientNetworkData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(cast_length!("channelCount", self.channels.len())?); + + for channel in self.channels.iter().take(CHANNELS_MAX) { + channel.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.channels.len() * CLIENT_CHANNEL_SIZE + } +} + +impl<'de> Decode<'de> for ClientNetworkData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let channel_count = cast_length!("channelCount", src.read_u32())?; + + if channel_count > CHANNELS_MAX { + return Err(invalid_field_err!("channelCount", "invalid channel count")); + } + + let mut channels = Vec::with_capacity(channel_count); + for _ in 0..channel_count { + channels.push(ChannelDef::decode(src)?); + } + + Ok(Self { channels }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerNetworkData { + pub channel_ids: Vec, + pub io_channel: u16, +} + +impl ServerNetworkData { + const NAME: &'static str = "ServerNetworkData"; + + const FIXED_PART_SIZE: usize = SERVER_IO_CHANNEL_SIZE + SERVER_CHANNEL_COUNT_SIZE; + + fn padding_needed(&self) -> bool { + self.channel_ids.len().is_odd() + } +} + +impl Encode for ServerNetworkData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.io_channel); + dst.write_u16(cast_length!("channelIdLen", self.channel_ids.len())?); + + for channel_id in self.channel_ids.iter() { + dst.write_u16(*channel_id); + } + + // The size in bytes of the Server Network Data structure MUST be a multiple of 4. + // If the channelCount field contains an odd value, then the size of the channelIdArray + // (and by implication the entire Server Network Data structure) will not be a multiple of 4. + // In this scenario, the Pad field MUST be present and it is used to add an additional + // 2 bytes to the size of the Server Network Data structure. + if self.padding_needed() { + write_padding!(dst, 2); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let padding_size = if self.padding_needed() { 2 } else { 0 }; + + Self::FIXED_PART_SIZE + self.channel_ids.len() * SERVER_CHANNEL_SIZE + padding_size + } +} + +impl<'de> Decode<'de> for ServerNetworkData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let io_channel = src.read_u16(); + let channel_count = cast_length!("channelCount", src.read_u16())?; + + ensure_size!(in: src, size: channel_count * 2); + let mut channel_ids = Vec::with_capacity(channel_count); + for _ in 0..channel_count { + channel_ids.push(src.read_u16()); + } + + let result = Self { + io_channel, + channel_ids, + }; + + if src.len() >= 2 { + read_padding!(src, 2); + } + + Ok(result) + } +} + +/// Channel Definition Structure (CHANNEL_DEF) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChannelDef { + pub name: ChannelName, + pub options: ChannelOptions, +} + +impl ChannelDef { + const NAME: &'static str = "ChannelDef"; + + const FIXED_PART_SIZE: usize = CLIENT_CHANNEL_SIZE; +} + +impl Encode for ChannelDef { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_slice(self.name.as_bytes()); + dst.write_u32(self.options.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ChannelDef { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let name = src.read_array(); + let name = ChannelName::new(name); + + let options = ChannelOptions::from_bits(src.read_u32()) + .ok_or_else(|| invalid_field_err!("options", "invalid channel options"))?; + + Ok(Self { name, options }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ChannelOptions: u32 { + const INITIALIZED = 0x8000_0000; + const ENCRYPT_RDP = 0x4000_0000; + const ENCRYPT_SC = 0x2000_0000; + const ENCRYPT_CS = 0x1000_0000; + const PRI_HIGH = 0x0800_0000; + const PRI_MED = 0x0400_0000; + const PRI_LOW = 0x0200_0000; + const COMPRESS_RDP = 0x0080_0000; + const COMPRESS = 0x0040_0000; + const SHOW_PROTOCOL = 0x0020_0000; + const REMOTE_CONTROL_PERSISTENT = 0x0010_0000; + } +} + +#[derive(Debug, Error)] +pub enum NetworkDataError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("UTF-8 error")] + Utf8Error(#[from] str::Utf8Error), + #[error("invalid channel options field")] + InvalidChannelOptions, + #[error("invalid channel count field")] + InvalidChannelCount, +} diff --git a/crates/ironrdp-pdu/src/gcc/security_data.rs b/crates/ironrdp-pdu/src/gcc/security_data.rs new file mode 100644 index 00000000..a33b520b --- /dev/null +++ b/crates/ironrdp-pdu/src/gcc/security_data.rs @@ -0,0 +1,233 @@ +use std::io; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; +use thiserror::Error; + +const CLIENT_ENCRYPTION_METHODS_SIZE: usize = 4; +const CLIENT_EXT_ENCRYPTION_METHODS_SIZE: usize = 4; + +const SERVER_ENCRYPTION_METHOD_SIZE: usize = 4; +const SERVER_ENCRYPTION_LEVEL_SIZE: usize = 4; +const SERVER_RANDOM_LEN_SIZE: usize = 4; +const SERVER_CERT_LEN_SIZE: usize = 4; +const SERVER_RANDOM_LEN: usize = 0x20; +const MAX_SERVER_CERT_LEN: usize = 1024; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientSecurityData { + pub encryption_methods: EncryptionMethod, + pub ext_encryption_methods: u32, +} + +impl ClientSecurityData { + const NAME: &'static str = "ClientSecurityData"; + + const FIXED_PART_SIZE: usize = CLIENT_ENCRYPTION_METHODS_SIZE + CLIENT_EXT_ENCRYPTION_METHODS_SIZE; + + pub fn no_security() -> Self { + Self { + encryption_methods: EncryptionMethod::empty(), + ext_encryption_methods: 0, + } + } +} + +impl Encode for ClientSecurityData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.encryption_methods.bits()); + dst.write_u32(self.ext_encryption_methods); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ClientSecurityData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let encryption_methods = EncryptionMethod::from_bits(src.read_u32()) + .ok_or_else(|| invalid_field_err!("encryptionMethods", "invalid encryption methods"))?; + let ext_encryption_methods = src.read_u32(); + + Ok(Self { + encryption_methods, + ext_encryption_methods, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerSecurityData { + pub encryption_method: EncryptionMethod, + pub encryption_level: EncryptionLevel, + pub server_random: Option<[u8; SERVER_RANDOM_LEN]>, + pub server_cert: Vec, +} + +impl ServerSecurityData { + const NAME: &'static str = "ServerSecurityData"; + + const FIXED_PART_SIZE: usize = SERVER_ENCRYPTION_METHOD_SIZE + SERVER_ENCRYPTION_LEVEL_SIZE; + + pub fn no_security() -> Self { + Self { + encryption_method: EncryptionMethod::empty(), + encryption_level: EncryptionLevel::None, + server_random: None, + server_cert: Vec::new(), + } + } +} + +impl Encode for ServerSecurityData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.encryption_method.bits()); + dst.write_u32(self.encryption_level.as_u32()); + + if self.encryption_method.is_empty() && self.encryption_level == EncryptionLevel::None { + if self.server_random.is_some() || !self.server_cert.is_empty() { + Err(invalid_field_err!("serverRandom", "An encryption method and encryption level is none, but the server random or certificate is not empty")) + } else { + Ok(()) + } + } else { + let server_random_len = match self.server_random { + Some(ref server_random) => server_random.len(), + None => 0, + }; + dst.write_u32(cast_length!("serverRandomLen", server_random_len)?); + dst.write_u32(cast_length!("serverCertLen", self.server_cert.len())?); + + if let Some(ref server_random) = self.server_random { + dst.write_slice(server_random.as_ref()); + } + dst.write_slice(self.server_cert.as_ref()); + + Ok(()) + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let mut size = Self::FIXED_PART_SIZE; + + if let Some(ref server_random) = self.server_random { + size += SERVER_RANDOM_LEN_SIZE + server_random.len(); + } + if !self.server_cert.is_empty() { + size += SERVER_CERT_LEN_SIZE + self.server_cert.len(); + } + + size + } +} + +impl<'de> Decode<'de> for ServerSecurityData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let encryption_method = EncryptionMethod::from_bits(src.read_u32()) + .ok_or_else(|| invalid_field_err!("encryptionMethod", "invalid encryption method"))?; + let encryption_level = EncryptionLevel::from_u32(src.read_u32()) + .ok_or_else(|| invalid_field_err!("encryptionLevel", "invalid encryption level"))?; + + let (server_random, server_cert) = if encryption_method.is_empty() && encryption_level == EncryptionLevel::None + { + (None, Vec::new()) + } else { + ensure_size!(in: src, size: 4 + 4); + + let server_random_len: usize = cast_length!("serverRandomLen", src.read_u32())?; + if server_random_len != SERVER_RANDOM_LEN { + return Err(invalid_field_err!("serverRandomLen", "Invalid server random length")); + } + + let server_cert_len = cast_length!("serverCertLen", src.read_u32())?; + + if server_cert_len > MAX_SERVER_CERT_LEN { + return Err(invalid_field_err!("serverCetLen", "Invalid server certificate length")); + } + + ensure_size!(in: src, size: SERVER_RANDOM_LEN); + let server_random = src.read_array(); + + ensure_size!(in: src, size: server_cert_len); + let server_cert = src.read_slice(server_cert_len); + + (Some(server_random), server_cert.into()) + }; + + Ok(Self { + encryption_method, + encryption_level, + server_random, + server_cert, + }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct EncryptionMethod: u32 { + const BIT_40 = 0x0000_0001; + const BIT_128 = 0x0000_0002; + const BIT_56 = 0x0000_0008; + const FIPS = 0x0000_0010; + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum EncryptionLevel { + None = 0, + Low = 1, + ClientCompatible = 2, + High = 3, + Fips = 4, +} + +impl EncryptionLevel { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[derive(Debug, Error)] +pub enum SecurityDataError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("invalid encryption methods field")] + InvalidEncryptionMethod, + #[error("invalid encryption level field")] + InvalidEncryptionLevel, + #[error("invalid server random length field: {0}")] + InvalidServerRandomLen(u32), + #[error("invalid input: {0}")] + InvalidInput(String), + #[error("invalid server certificate length: {0}")] + InvalidServerCertificateLen(u32), +} diff --git a/crates/ironrdp-pdu/src/geometry.rs b/crates/ironrdp-pdu/src/geometry.rs new file mode 100644 index 00000000..b78d87ca --- /dev/null +++ b/crates/ironrdp-pdu/src/geometry.rs @@ -0,0 +1,264 @@ +use core::cmp::{max, min}; + +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +pub(crate) mod private { + pub struct BaseRectangle { + pub left: u16, + pub top: u16, + pub right: u16, + pub bottom: u16, + } + + impl BaseRectangle { + pub fn empty() -> Self { + Self { + left: 0, + top: 0, + right: 0, + bottom: 0, + } + } + } + + pub trait RectangleImpl: Sized { + fn from_base(rect: BaseRectangle) -> Self; + fn to_base(&self) -> BaseRectangle; + + fn left(&self) -> u16; + fn top(&self) -> u16; + fn right(&self) -> u16; + fn bottom(&self) -> u16; + } +} + +use private::{BaseRectangle, RectangleImpl}; + +pub trait Rectangle: RectangleImpl { + fn width(&self) -> u16; + fn height(&self) -> u16; + + fn empty() -> Self { + Self::from_base(BaseRectangle::empty()) + } + + fn union_all(rectangles: &[Self]) -> Self { + Self::from_base(BaseRectangle { + left: rectangles.iter().map(|r| r.left()).min().unwrap_or(0), + top: rectangles.iter().map(|r| r.top()).min().unwrap_or(0), + right: rectangles.iter().map(|r| r.right()).max().unwrap_or(0), + bottom: rectangles.iter().map(|r| r.bottom()).max().unwrap_or(0), + }) + } + + fn intersect(&self, other: &Self) -> Option { + let a = self.to_base(); + let b = other.to_base(); + + let result = BaseRectangle { + left: max(a.left, b.left), + top: max(a.top, b.top), + right: min(a.right, b.right), + bottom: min(a.bottom, b.bottom), + }; + + if result.left <= result.right && result.top <= result.bottom { + Some(Self::from_base(result)) + } else { + None + } + } + + #[must_use] + fn union(&self, other: &Self) -> Self { + let a = self.to_base(); + let b = other.to_base(); + + let result = BaseRectangle { + left: min(a.left, b.left), + top: min(a.top, b.top), + right: max(a.right, b.right), + bottom: max(a.bottom, b.bottom), + }; + + Self::from_base(result) + } +} + +/// An **inclusive** rectangle. +/// +/// This struct is defined as an **inclusive** rectangle. +/// That is, the pixel at coordinate (right, bottom) is included in the rectangle. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InclusiveRectangle { + pub left: u16, + pub top: u16, + pub right: u16, + pub bottom: u16, +} + +/// An **exclusive** rectangle. +/// This struct is defined as an **exclusive** rectangle. +/// That is, the pixel at coordinate (right, bottom) is not included in the rectangle. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExclusiveRectangle { + pub left: u16, + pub top: u16, + pub right: u16, + pub bottom: u16, +} + +macro_rules! impl_rectangle { + ($type:ty) => { + impl RectangleImpl for $type { + fn from_base(rect: BaseRectangle) -> Self { + Self { + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + } + } + + fn to_base(&self) -> BaseRectangle { + BaseRectangle { + left: self.left, + top: self.top, + right: self.right, + bottom: self.bottom, + } + } + + fn left(&self) -> u16 { + self.left + } + fn top(&self) -> u16 { + self.top + } + fn right(&self) -> u16 { + self.right + } + fn bottom(&self) -> u16 { + self.bottom + } + } + }; +} + +impl_rectangle!(InclusiveRectangle); +impl_rectangle!(ExclusiveRectangle); + +impl Rectangle for InclusiveRectangle { + /// INVARIANT: `0 < output (width)` + fn width(&self) -> u16 { + self.right - self.left + 1 + } + + /// INVARIANT: `0 < output (height)` + fn height(&self) -> u16 { + self.bottom - self.top + 1 + } +} + +impl Rectangle for ExclusiveRectangle { + fn width(&self) -> u16 { + self.right - self.left + } + + fn height(&self) -> u16 { + self.bottom - self.top + } +} + +impl InclusiveRectangle { + const NAME: &'static str = "InclusiveRectangle"; + + pub const FIXED_PART_SIZE: usize = 2 /* left */ + 2 /* top */ + 2 /* right */ + 2 /* bottom */; + + pub const ENCODED_SIZE: usize = Self::FIXED_PART_SIZE; +} + +impl Encode for InclusiveRectangle { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.left); + dst.write_u16(self.top); + dst.write_u16(self.right); + dst.write_u16(self.bottom); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for InclusiveRectangle { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let left = src.read_u16(); + let top = src.read_u16(); + let right = src.read_u16(); + let bottom = src.read_u16(); + + Ok(Self { + left, + top, + right, + bottom, + }) + } +} + +impl ExclusiveRectangle { + const NAME: &'static str = "ExclusiveRectangle"; + const FIXED_PART_SIZE: usize = 2 /* left */ + 2 /* top */ + 2 /* right */ + 2 /* bottom */; + + pub const ENCODED_SIZE: usize = Self::FIXED_PART_SIZE; +} + +impl Encode for ExclusiveRectangle { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.left); + dst.write_u16(self.top); + dst.write_u16(self.right); + dst.write_u16(self.bottom); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ExclusiveRectangle { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let left = src.read_u16(); + let top = src.read_u16(); + let right = src.read_u16(); + let bottom = src.read_u16(); + + Ok(Self { + left, + top, + right, + bottom, + }) + } +} diff --git a/crates/ironrdp-pdu/src/input/fast_path.rs b/crates/ironrdp-pdu/src/input/fast_path.rs new file mode 100644 index 00000000..abbe7816 --- /dev/null +++ b/crates/ironrdp-pdu/src/input/fast_path.rs @@ -0,0 +1,331 @@ +use bit_field::BitField as _; +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, other_err, Decode, DecodeResult, Encode, + EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use crate::fast_path::EncryptionFlags; +use crate::input::{MousePdu, MouseRelPdu, MouseXPdu}; +use crate::per; + +/// Implements the Fast-Path RDP message header PDU. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FastPathInputHeader { + pub flags: EncryptionFlags, + pub data_length: usize, + pub num_events: u8, +} + +impl FastPathInputHeader { + const NAME: &'static str = "FastPathInputHeader"; + + const FIXED_PART_SIZE: usize = 1 /* header */; +} + +impl Encode for FastPathInputHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let mut header = 0u8; + header.set_bits(0..2, 0); // fast-path action + if self.num_events < 16 { + header.set_bits(2..7, self.num_events); + } + header.set_bits(6..8, self.flags.bits()); + dst.write_u8(header); + + per::write_length(dst, cast_length!("len", self.data_length + self.size())?); + if self.num_events > 15 { + dst.write_u8(self.num_events); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let num_events_length = if self.num_events < 16 { 0 } else { 1 }; + Self::FIXED_PART_SIZE + per::sizeof_length(self.data_length + num_events_length + 1) + num_events_length + } +} + +impl<'de> Decode<'de> for FastPathInputHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let header = src.read_u8(); + let flags = EncryptionFlags::from_bits_truncate(header.get_bits(6..8)); + let mut num_events = header.get_bits(2..6); + let (length, sizeof_length) = per::read_length(src).map_err(|e| other_err!("perLen", source: e))?; + + if !flags.is_empty() { + return Err(invalid_field_err!("flags", "encryption not supported")); + } + + let num_events_length = if num_events == 0 { + ensure_size!(in: src, size: 1); + num_events = src.read_u8(); + 1 + } else { + 0 + }; + + let data_length = usize::from(length) - sizeof_length - 1 - num_events_length; + + Ok(FastPathInputHeader { + flags, + data_length, + num_events, + }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(u8)] +pub enum FastpathInputEventType { + ScanCode = 0x0000, + Mouse = 0x0001, + MouseX = 0x0002, + Sync = 0x0003, + Unicode = 0x0004, + MouseRel = 0x0005, + QoeTimestamp = 0x0006, +} + +impl FastpathInputEventType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FastPathInputEvent { + KeyboardEvent(KeyboardFlags, u8), + UnicodeKeyboardEvent(KeyboardFlags, u16), + MouseEvent(MousePdu), + MouseEventEx(MouseXPdu), + MouseEventRel(MouseRelPdu), + QoeEvent(u32), + SyncEvent(SynchronizeFlags), +} + +impl FastPathInputEvent { + const NAME: &'static str = "FastPathInputEvent"; + + const FIXED_PART_SIZE: usize = 1 /* header */; +} + +impl Encode for FastPathInputEvent { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let mut header = 0u8; + let (flags, code) = match self { + FastPathInputEvent::KeyboardEvent(flags, _) => (flags.bits(), FastpathInputEventType::ScanCode), + FastPathInputEvent::UnicodeKeyboardEvent(flags, _) => (flags.bits(), FastpathInputEventType::Unicode), + FastPathInputEvent::MouseEvent(_) => (0, FastpathInputEventType::Mouse), + FastPathInputEvent::MouseEventEx(_) => (0, FastpathInputEventType::MouseX), + FastPathInputEvent::MouseEventRel(_) => (0, FastpathInputEventType::MouseRel), + FastPathInputEvent::QoeEvent(_) => (0, FastpathInputEventType::QoeTimestamp), + FastPathInputEvent::SyncEvent(flags) => (flags.bits(), FastpathInputEventType::Sync), + }; + header.set_bits(0..5, flags); + header.set_bits(5..8, code.as_u8()); + dst.write_u8(header); + match self { + FastPathInputEvent::KeyboardEvent(_, code) => { + dst.write_u8(*code); + } + FastPathInputEvent::UnicodeKeyboardEvent(_, code) => { + dst.write_u16(*code); + } + FastPathInputEvent::MouseEvent(pdu) => { + pdu.encode(dst)?; + } + FastPathInputEvent::MouseEventEx(pdu) => { + pdu.encode(dst)?; + } + FastPathInputEvent::QoeEvent(stamp) => { + dst.write_u32(*stamp); + } + _ => {} + }; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + match self { + FastPathInputEvent::KeyboardEvent(_, _) => 1, + FastPathInputEvent::UnicodeKeyboardEvent(_, _) => 2, + FastPathInputEvent::MouseEvent(pdu) => pdu.size(), + FastPathInputEvent::MouseEventEx(pdu) => pdu.size(), + FastPathInputEvent::MouseEventRel(pdu) => pdu.size(), + FastPathInputEvent::QoeEvent(_) => 4, + FastPathInputEvent::SyncEvent(_) => 0, + } + } +} + +impl<'de> Decode<'de> for FastPathInputEvent { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let header = src.read_u8(); + let flags = header.get_bits(0..5); + let code = header.get_bits(5..8); + let code: FastpathInputEventType = FastpathInputEventType::from_u8(code) + .ok_or_else(|| invalid_field_err!("code", "input event code unsupported"))?; + let event = match code { + FastpathInputEventType::ScanCode => { + ensure_size!(in: src, size: 1); + let code = src.read_u8(); + let flags = KeyboardFlags::from_bits(flags) + .ok_or_else(|| invalid_field_err!("flags", "input keyboard flags unsupported"))?; + FastPathInputEvent::KeyboardEvent(flags, code) + } + FastpathInputEventType::Mouse => { + let mouse_event = MousePdu::decode(src)?; + FastPathInputEvent::MouseEvent(mouse_event) + } + FastpathInputEventType::MouseX => { + let mouse_event = MouseXPdu::decode(src)?; + FastPathInputEvent::MouseEventEx(mouse_event) + } + FastpathInputEventType::MouseRel => { + let mouse_event = MouseRelPdu::decode(src)?; + FastPathInputEvent::MouseEventRel(mouse_event) + } + FastpathInputEventType::Sync => { + let flags = SynchronizeFlags::from_bits(flags) + .ok_or_else(|| invalid_field_err!("flags", "input synchronize flags unsupported"))?; + FastPathInputEvent::SyncEvent(flags) + } + FastpathInputEventType::Unicode => { + ensure_size!(in: src, size: 2); + let code = src.read_u16(); + let flags = KeyboardFlags::from_bits(flags) + .ok_or_else(|| invalid_field_err!("flags", "input keyboard flags unsupported"))?; + FastPathInputEvent::UnicodeKeyboardEvent(flags, code) + } + FastpathInputEventType::QoeTimestamp => { + ensure_size!(in: src, size: 4); + let code = src.read_u32(); + FastPathInputEvent::QoeEvent(code) + } + }; + Ok(event) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct KeyboardFlags: u8 { + const RELEASE = 0x01; + const EXTENDED = 0x02; + const EXTENDED1 = 0x04; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct SynchronizeFlags: u8 { + const SCROLL_LOCK = 0x01; + const NUM_LOCK = 0x02; + const CAPS_LOCK = 0x04; + const KANA_LOCK = 0x08; + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FastPathInput( + /// INVARIANT: (1..=255).contains(len()) = at least one, and at most 255 elements. + Vec, +); + +impl FastPathInput { + const NAME: &'static str = "FastPathInput"; + + pub fn new(input_events: Vec) -> DecodeResult { + // Ensure the invariant on `input_events.len()` is respected. + if !(1..=255usize).contains(&input_events.len()) { + return Err(invalid_field_err!("nEvents", "invalid number of input events")); + } + + Ok(Self(input_events)) + } + + pub fn single(input_event: FastPathInputEvent) -> Self { + // A single element upholds the invariant. + Self(vec![input_event]) + } + + pub fn input_events(&self) -> &[FastPathInputEvent] { + &self.0 + } +} + +impl Encode for FastPathInput { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + if self.0.is_empty() { + return Err(other_err!("Empty fast-path input")); + } + + let data_length = self.0.iter().map(Encode::size).sum::(); + let header = FastPathInputHeader { + num_events: u8::try_from(self.0.len()).expect("per invariant (1..=255).contains(num_events.len())"), + flags: EncryptionFlags::empty(), + data_length, + }; + header.encode(dst)?; + + for event in self.0.iter() { + event.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let data_length = self.0.iter().map(Encode::size).sum::(); + let header = FastPathInputHeader { + num_events: u8::try_from(self.0.len()) + .expect("INVARIANT: num_events is within the range of 1 to 255, inclusive"), + flags: EncryptionFlags::empty(), + data_length, + }; + header.size() + data_length + } +} + +impl<'de> Decode<'de> for FastPathInput { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let header = FastPathInputHeader::decode(src)?; + let events = core::iter::repeat_with(|| FastPathInputEvent::decode(src)) + .take(usize::from(header.num_events)) + .collect::, _>>()?; + + Self::new(events) + } +} diff --git a/crates/ironrdp-pdu/src/input/mod.rs b/crates/ironrdp-pdu/src/input/mod.rs new file mode 100644 index 00000000..a85b5195 --- /dev/null +++ b/crates/ironrdp-pdu/src/input/mod.rs @@ -0,0 +1,201 @@ +use std::io; + +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, write_padding, Decode, + DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; +use thiserror::Error; + +pub mod fast_path; +pub mod mouse; +pub mod mouse_rel; +pub mod mouse_x; +pub mod scan_code; +pub mod sync; +pub mod unicode; +pub mod unused; + +pub use self::mouse::MousePdu; +pub use self::mouse_rel::MouseRelPdu; +pub use self::mouse_x::MouseXPdu; +pub use self::scan_code::ScanCodePdu; +pub use self::sync::SyncPdu; +pub use self::unicode::UnicodePdu; +pub use self::unused::UnusedPdu; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InputEventPdu(pub Vec); + +impl InputEventPdu { + const NAME: &'static str = "InputEventPdu"; + + const FIXED_PART_SIZE: usize = 4 /* nEvents */; +} + +impl Encode for InputEventPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(cast_length!("input events count", self.0.len())?); + write_padding!(dst, 2); + + for event in self.0.iter() { + event.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + 4 + self.0.iter().map(Encode::size).sum::() + } +} + +impl<'de> Decode<'de> for InputEventPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let number_of_events = usize::from(src.read_u16()); + read_padding!(src, 2); + + let events = core::iter::repeat_with(|| InputEvent::decode(src)) + .take(number_of_events) + .collect::, _>>()?; + + Ok(Self(events)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InputEvent { + Sync(SyncPdu), + Unused(UnusedPdu), + ScanCode(ScanCodePdu), + Unicode(UnicodePdu), + Mouse(MousePdu), + MouseX(MouseXPdu), + MouseRel(MouseRelPdu), +} + +impl InputEvent { + const NAME: &'static str = "InputEvent"; + + const FIXED_PART_SIZE: usize = 4 /* eventTime */ + 2 /* eventType */; +} + +impl Encode for InputEvent { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(0); // event time is ignored by a server + dst.write_u16(InputEventType::from(self).as_u16()); + + match self { + Self::Sync(pdu) => pdu.encode(dst), + Self::Unused(pdu) => pdu.encode(dst), + Self::ScanCode(pdu) => pdu.encode(dst), + Self::Unicode(pdu) => pdu.encode(dst), + Self::Mouse(pdu) => pdu.encode(dst), + Self::MouseX(pdu) => pdu.encode(dst), + Self::MouseRel(pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + match self { + Self::Sync(pdu) => pdu.size(), + Self::Unused(pdu) => pdu.size(), + Self::ScanCode(pdu) => pdu.size(), + Self::Unicode(pdu) => pdu.size(), + Self::Mouse(pdu) => pdu.size(), + Self::MouseX(pdu) => pdu.size(), + Self::MouseRel(pdu) => pdu.size(), + } + } +} + +impl<'de> Decode<'de> for InputEvent { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _event_time = src.read_u32(); // ignored by a server + let event_type = src.read_u16(); + let event_type = InputEventType::from_u16(event_type) + .ok_or_else(|| invalid_field_err!("eventType", "invalid input event type"))?; + + match event_type { + InputEventType::Sync => Ok(Self::Sync(SyncPdu::decode(src)?)), + InputEventType::Unused => Ok(Self::Unused(UnusedPdu::decode(src)?)), + InputEventType::ScanCode => Ok(Self::ScanCode(ScanCodePdu::decode(src)?)), + InputEventType::Unicode => Ok(Self::Unicode(UnicodePdu::decode(src)?)), + InputEventType::Mouse => Ok(Self::Mouse(MousePdu::decode(src)?)), + InputEventType::MouseX => Ok(Self::MouseX(MouseXPdu::decode(src)?)), + InputEventType::MouseRel => Ok(Self::MouseRel(MouseRelPdu::decode(src)?)), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive)] +#[repr(u16)] +enum InputEventType { + Sync = 0x0000, + Unused = 0x0002, + ScanCode = 0x0004, + Unicode = 0x0005, + Mouse = 0x8001, + MouseX = 0x8002, + MouseRel = 0x8004, +} + +impl InputEventType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +impl From<&InputEvent> for InputEventType { + fn from(event: &InputEvent) -> Self { + match event { + InputEvent::Sync(_) => Self::Sync, + InputEvent::Unused(_) => Self::Unused, + InputEvent::ScanCode(_) => Self::ScanCode, + InputEvent::Unicode(_) => Self::Unicode, + InputEvent::Mouse(_) => Self::Mouse, + InputEvent::MouseX(_) => Self::MouseX, + InputEvent::MouseRel(_) => Self::MouseRel, + } + } +} + +#[derive(Debug, Error)] +pub enum InputEventError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("invalid Input Event type: {0}")] + InvalidInputEventType(u16), + #[error("encryption not supported")] + EncryptionNotSupported, + #[error("event code not supported {0}")] + EventCodeUnsupported(u8), + #[error("keyboard flags not supported {0}")] + KeyboardFlagsUnsupported(u8), + #[error("synchronize flags not supported {0}")] + SynchronizeFlagsUnsupported(u8), + #[error("Fast-Path Input Event PDU is empty")] + EmptyFastPathInput, +} diff --git a/crates/ironrdp-pdu/src/input/mouse.rs b/crates/ironrdp-pdu/src/input/mouse.rs new file mode 100644 index 00000000..3125739e --- /dev/null +++ b/crates/ironrdp-pdu/src/input/mouse.rs @@ -0,0 +1,99 @@ +use bitflags::bitflags; +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MousePdu { + pub flags: PointerFlags, + pub number_of_wheel_rotation_units: i16, + pub x_position: u16, + pub y_position: u16, +} + +impl MousePdu { + const NAME: &'static str = "MousePdu"; + + const FIXED_PART_SIZE: usize = 2 /* flags */ + 2 /* x */ + 2 /* y */; +} + +impl Encode for MousePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let wheel_negative_bit = if self.number_of_wheel_rotation_units < 0 { + PointerFlags::WHEEL_NEGATIVE.bits() + } else { + PointerFlags::empty().bits() + }; + + #[expect( + clippy::as_conversions, + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + reason = "truncation intended" + )] + let truncated_wheel_rotation_units = self.number_of_wheel_rotation_units as u8; + let wheel_rotations_bits = u16::from(truncated_wheel_rotation_units); + + let flags = self.flags.bits() | wheel_negative_bit | wheel_rotations_bits; + + dst.write_u16(flags); + dst.write_u16(self.x_position); + dst.write_u16(self.y_position); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for MousePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags_raw = src.read_u16(); + + let flags = PointerFlags::from_bits_truncate(flags_raw); + + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "truncation intended" + )] + let wheel_rotations_bits = flags_raw as u8; + + let number_of_wheel_rotation_units = if flags.contains(PointerFlags::WHEEL_NEGATIVE) { + -i16::from(wheel_rotations_bits) + } else { + i16::from(wheel_rotations_bits) + }; + + let x_position = src.read_u16(); + let y_position = src.read_u16(); + + Ok(Self { + flags, + number_of_wheel_rotation_units, + x_position, + y_position, + }) + } +} +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct PointerFlags: u16 { + const WHEEL_NEGATIVE = 0x0100; + const VERTICAL_WHEEL = 0x0200; + const HORIZONTAL_WHEEL = 0x0400; + const MOVE = 0x0800; + const LEFT_BUTTON = 0x1000; + const RIGHT_BUTTON = 0x2000; + const MIDDLE_BUTTON_OR_WHEEL = 0x4000; + const DOWN = 0x8000; + } +} diff --git a/crates/ironrdp-pdu/src/input/mouse_rel.rs b/crates/ironrdp-pdu/src/input/mouse_rel.rs new file mode 100644 index 00000000..a1812519 --- /dev/null +++ b/crates/ironrdp-pdu/src/input/mouse_rel.rs @@ -0,0 +1,64 @@ +use bitflags::bitflags; +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MouseRelPdu { + pub flags: PointerRelFlags, + pub x_delta: i16, + pub y_delta: i16, +} + +impl MouseRelPdu { + const NAME: &'static str = "MouseRelPdu"; + + const FIXED_PART_SIZE: usize = 2 /* flags */ + 2 /* x */ + 2 /* y */; +} + +impl Encode for MouseRelPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.flags.bits()); + dst.write_i16(self.x_delta); + dst.write_i16(self.y_delta); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for MouseRelPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = PointerRelFlags::from_bits_truncate(src.read_u16()); + let x_delta = src.read_i16(); + let y_delta = src.read_i16(); + + Ok(Self { + flags, + x_delta, + y_delta, + }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct PointerRelFlags: u16 { + const MOVE = 0x0800; + const DOWN = 0x8000; + const BUTTON1 = 0x1000; + const BUTTON2 = 0x2000; + const BUTTON3 = 0x4000; + const XBUTTON1 = 0x0001; + const XBUTTON2 = 0x0002; + } +} diff --git a/crates/ironrdp-pdu/src/input/mouse_x.rs b/crates/ironrdp-pdu/src/input/mouse_x.rs new file mode 100644 index 00000000..500e97c7 --- /dev/null +++ b/crates/ironrdp-pdu/src/input/mouse_x.rs @@ -0,0 +1,60 @@ +use bitflags::bitflags; +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MouseXPdu { + pub flags: PointerXFlags, + pub x_position: u16, + pub y_position: u16, +} + +impl MouseXPdu { + const NAME: &'static str = "MouseXPdu"; + + const FIXED_PART_SIZE: usize = 2 /* flags */ + 2 /* x */ + 2 /* y */; +} + +impl Encode for MouseXPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.flags.bits()); + dst.write_u16(self.x_position); + dst.write_u16(self.y_position); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for MouseXPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = PointerXFlags::from_bits_truncate(src.read_u16()); + let x_position = src.read_u16(); + let y_position = src.read_u16(); + + Ok(Self { + flags, + x_position, + y_position, + }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct PointerXFlags: u16 { + const DOWN = 0x8000; + const BUTTON1 = 0x0001; + const BUTTON2 = 0x0002; + } +} diff --git a/crates/ironrdp-pdu/src/input/scan_code.rs b/crates/ironrdp-pdu/src/input/scan_code.rs new file mode 100644 index 00000000..0798866c --- /dev/null +++ b/crates/ironrdp-pdu/src/input/scan_code.rs @@ -0,0 +1,59 @@ +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, read_padding, write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScanCodePdu { + pub flags: KeyboardFlags, + pub key_code: u16, +} + +impl ScanCodePdu { + const NAME: &'static str = "ScanCodePdu"; + + const FIXED_PART_SIZE: usize = 2 /* flags */ + 2 /* keycode */ + 2 /* padding */; +} + +impl Encode for ScanCodePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.flags.bits()); + dst.write_u16(self.key_code); + write_padding!(dst, 2); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ScanCodePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = KeyboardFlags::from_bits_truncate(src.read_u16()); + let key_code = src.read_u16(); + read_padding!(src, 2); + + Ok(Self { flags, key_code }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct KeyboardFlags: u16 { + const EXTENDED = 0x0100; + const EXTENDED_1 = 0x0200; + const DOWN = 0x4000; + const RELEASE = 0x8000; + } +} diff --git a/crates/ironrdp-pdu/src/input/sync.rs b/crates/ironrdp-pdu/src/input/sync.rs new file mode 100644 index 00000000..81453937 --- /dev/null +++ b/crates/ironrdp-pdu/src/input/sync.rs @@ -0,0 +1,56 @@ +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, read_padding, write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SyncPdu { + pub flags: SyncToggleFlags, +} + +impl SyncPdu { + const NAME: &'static str = "SyncPdu"; + + const FIXED_PART_SIZE: usize = 2 /* padding */ + 4 /* flags */; +} + +impl Encode for SyncPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + write_padding!(dst, 2); + dst.write_u32(self.flags.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for SyncPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + read_padding!(src, 2); + let flags = SyncToggleFlags::from_bits_truncate(src.read_u32()); + + Ok(Self { flags }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct SyncToggleFlags: u32 { + const SCROLL_LOCK = 0x1; + const NUM_LOCK = 0x2; + const CAPS_LOCK = 0x4; + const KANA_LOCK = 0x8; + } +} diff --git a/crates/ironrdp-pdu/src/input/unicode.rs b/crates/ironrdp-pdu/src/input/unicode.rs new file mode 100644 index 00000000..37ce8d67 --- /dev/null +++ b/crates/ironrdp-pdu/src/input/unicode.rs @@ -0,0 +1,56 @@ +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, read_padding, write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnicodePdu { + pub flags: KeyboardFlags, + pub unicode_code: u16, +} + +impl UnicodePdu { + const NAME: &'static str = "UnicodePdu"; + + const FIXED_PART_SIZE: usize = 2 /* flags */ + 2 /* code */ + 2 /* padding */; +} + +impl Encode for UnicodePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.flags.bits()); + dst.write_u16(self.unicode_code); + write_padding!(dst, 2); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for UnicodePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = KeyboardFlags::from_bits_truncate(src.read_u16()); + let unicode_code = src.read_u16(); + read_padding!(src, 2); + + Ok(Self { flags, unicode_code }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct KeyboardFlags: u16 { + const RELEASE = 0x8000; + } +} diff --git a/crates/ironrdp-pdu/src/input/unused.rs b/crates/ironrdp-pdu/src/input/unused.rs new file mode 100644 index 00000000..8e3a00ff --- /dev/null +++ b/crates/ironrdp-pdu/src/input/unused.rs @@ -0,0 +1,39 @@ +use ironrdp_core::{ + ensure_fixed_part_size, read_padding, write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnusedPdu; + +impl UnusedPdu { + const NAME: &'static str = "UnusedPdu"; + + const FIXED_PART_SIZE: usize = 6 /* padding */; +} + +impl Encode for UnusedPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + write_padding!(dst, 6); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for UnusedPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + read_padding!(src, 6); + Ok(Self) + } +} diff --git a/crates/ironrdp-pdu/src/lib.rs b/crates/ironrdp-pdu/src/lib.rs new file mode 100644 index 00000000..6d2a387d --- /dev/null +++ b/crates/ironrdp-pdu/src/lib.rs @@ -0,0 +1,262 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![allow(clippy::arithmetic_side_effects)] // FIXME: remove + +use core::fmt; + +// TODO(#583): uncomment once re-exports are removed. +// use ironrdp_core::{unexpected_message_type_err, DecodeResult, EncodeResult, ReadCursor}; +use ironrdp_error::Source; + +mod macros; + +pub mod codecs; +pub mod gcc; +pub mod geometry; +pub mod input; +pub mod mcs; +pub mod nego; +pub mod pcb; +pub mod rdp; +pub mod tpdu; +pub mod tpkt; +pub mod utf16; +pub mod utils; +pub mod x224; + +pub(crate) mod basic_output; +pub(crate) mod ber; +pub(crate) mod crypto; +pub(crate) mod per; + +pub use crate::basic_output::{bitmap, fast_path, pointer, surface_commands}; +pub use crate::rdp::vc::dvc; + +pub type PduResult = Result; + +pub type PduError = ironrdp_error::Error; + +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum PduErrorKind { + Encode, + Decode, + Other { description: &'static str }, +} + +pub trait PduErrorExt { + fn decode(context: &'static str, source: E) -> Self; + + fn encode(context: &'static str, source: E) -> Self; +} + +impl PduErrorExt for PduError { + fn decode(context: &'static str, source: E) -> Self { + Self::new(context, PduErrorKind::Decode).with_source(source) + } + + fn encode(context: &'static str, source: E) -> Self { + Self::new(context, PduErrorKind::Encode).with_source(source) + } +} + +impl core::error::Error for PduErrorKind {} + +impl fmt::Display for PduErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Encode => { + write!(f, "encode error") + } + Self::Decode => { + write!(f, "decode error") + } + Self::Other { description } => { + write!(f, "other ({description})") + } + } + } +} + +/// An RDP PDU. +pub trait Pdu { + /// Name associated to this PDU. + const NAME: &'static str; +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(u8)] +pub enum Action { + FastPath = 0x00, + X224 = 0x03, +} + +impl Action { + pub fn from_fp_output_header(fp_output_header: u8) -> Result { + match fp_output_header & 0b11 { + 0x00 => Ok(Self::FastPath), + 0x03 => Ok(Self::X224), + unknown_action_bits => Err(unknown_action_bits), + } + } + + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct PduInfo { + pub action: Action, + pub length: usize, +} + +/// Finds next RDP PDU size by reading the next few bytes. +pub fn find_size(bytes: &[u8]) -> DecodeResult> { + macro_rules! ensure_enough { + ($bytes:expr, $len:expr) => { + if $bytes.len() < $len { + return Ok(None); + } + }; + } + + ensure_enough!(bytes, 1); + let fp_output_header = bytes[0]; + + let action = Action::from_fp_output_header(fp_output_header) + .map_err(|unknown_action| unexpected_message_type_err("fpOutputHeader", unknown_action))?; + + match action { + Action::X224 => { + ensure_enough!(bytes, tpkt::TpktHeader::SIZE); + let tpkt = tpkt::TpktHeader::read(&mut ReadCursor::new(bytes))?; + + Ok(Some(PduInfo { + action, + length: tpkt.packet_length(), + })) + } + Action::FastPath => { + ensure_enough!(bytes, 2); + let a = bytes[1]; + + let fast_path_length = if a & 0x80 != 0 { + ensure_enough!(bytes, 3); + let b = bytes[2]; + + ((u16::from(a) & !0x80) << 8) + u16::from(b) + } else { + u16::from(a) + }; + + Ok(Some(PduInfo { + action, + length: usize::from(fast_path_length), + })) + } + } +} + +pub trait PduHint: Send + Sync + fmt::Debug + 'static { + /// Finds next PDU size by reading the next few bytes. + /// + /// Returns `Some((hint_matching, size))` if the size is known. + /// Returns `None` if the size cannot be determined yet. + fn find_size(&self, bytes: &[u8]) -> DecodeResult>; +} + +// Matches both X224 and FastPath pdus +#[derive(Clone, Copy, Debug)] +pub struct RdpHint; + +pub const RDP_HINT: RdpHint = RdpHint; + +impl PduHint for RdpHint { + fn find_size(&self, bytes: &[u8]) -> DecodeResult> { + find_size(bytes).map(|opt| opt.map(|info| (true, info.length))) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct X224Hint; + +pub const X224_HINT: X224Hint = X224Hint; + +impl PduHint for X224Hint { + fn find_size(&self, bytes: &[u8]) -> DecodeResult> { + match find_size(bytes)? { + Some(pdu_info) => { + let res = (pdu_info.action == Action::X224, pdu_info.length); + Ok(Some(res)) + } + None => Ok(None), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct FastPathHint; + +pub const FAST_PATH_HINT: FastPathHint = FastPathHint; + +impl PduHint for FastPathHint { + fn find_size(&self, bytes: &[u8]) -> DecodeResult> { + match find_size(bytes)? { + Some(pdu_info) => { + let res = (pdu_info.action == Action::FastPath, pdu_info.length); + Ok(Some(res)) + } + None => Ok(None), + } + } +} + +// Private! Used by the macros. +#[doc(hidden)] +pub use ironrdp_core; + +// -- Temporary re-exports to ease teleport’s migration to the newer versions -- // +// TODO(#583): remove once Teleport migrated to the newer item paths. +// NOTE: #[deprecated] has no effect on re-exports, so this is mostly for documenting the code at this point. +#[doc(hidden)] +#[deprecated(since = "0.1.0", note = "use ironrdp_core::{ReadCursor, WriteCursor}")] +pub mod cursor { + pub use ironrdp_core::{ReadCursor, WriteCursor}; +} + +#[doc(hidden)] +#[deprecated(since = "0.1.0", note = "use ironrdp_core::WriteBuf")] +pub mod write_buf { + pub use ironrdp_core::WriteBuf; +} + +#[doc(hidden)] +#[deprecated(since = "0.1.0", note = "use ironrdp_core")] +pub use ironrdp_core::*; + +#[doc(hidden)] +#[deprecated(since = "0.1.0")] +#[macro_export] +macro_rules! custom_err { + ( $description:expr, $source:expr $(,)? ) => {{ + $crate::PduError::new( + $description, + $crate::PduErrorKind::Other { + description: $description, + }, + ) + .with_source($source) + }}; + ( $source:expr $(,)? ) => {{ + $crate::custom_err!($crate::function!(), $source) + }}; +} + +#[doc(hidden)] +#[deprecated(since = "0.1.0", note = "use ironrdp_core::other_err")] +pub use crate::pdu_other_err as other_err; diff --git a/crates/ironrdp-pdu/src/macros.rs b/crates/ironrdp-pdu/src/macros.rs new file mode 100644 index 00000000..2bbfda73 --- /dev/null +++ b/crates/ironrdp-pdu/src/macros.rs @@ -0,0 +1,148 @@ +//! Helper macros for PDU encoding and decoding +//! +//! Some are exported and available to external crates + +#[macro_export] +macro_rules! decode_err { + ($source:expr $(,)? ) => { + <$crate::PduError as $crate::PduErrorExt>::decode($crate::ironrdp_core::function!(), $source) + }; +} + +#[macro_export] +macro_rules! encode_err { + ($source:expr $(,)? ) => { + <$crate::PduError as $crate::PduErrorExt>::encode($crate::ironrdp_core::function!(), $source) + }; +} + +#[macro_export] +macro_rules! pdu_other_err { + ( $description:expr, source: $source:expr $(,)? ) => {{ + $crate::PduError::new($description, $crate::PduErrorKind::Other { description: $description }).with_source($source) + }}; + ( $context:expr, $description:expr $(,)? ) => {{ + $crate::PduError::new($context, $crate::PduErrorKind::Other { description: $description }) + }}; + ( source: $source:expr $(,)? ) => {{ + $crate::pdu_other_err!($crate::ironrdp_core::function!(), "", source: $source) + }}; + ( $description:expr $(,)? ) => {{ + $crate::pdu_other_err!($crate::ironrdp_core::function!(), $description) + }}; +} + +// FIXME: some of these macros should be in ironrdp_core, and some should be private to ironrdp_pdu. + +/// Asserts that constant expressions evaluate to `true`. +/// +/// From +#[macro_export] +macro_rules! const_assert { + ($x:expr $(,)?) => { + #[allow(unknown_lints, eq_op)] + const _: [(); 0 - !{ + const ASSERT: bool = $x; + ASSERT + } as usize] = []; + }; +} + +/// Implements additional traits for a plain old data structure (POD). +#[macro_export] +macro_rules! impl_pdu_pod { + ($pdu_ty:ty) => { + impl $crate::ironrdp_core::IntoOwned for $pdu_ty { + type Owned = Self; + + fn into_owned(self) -> Self::Owned { + self + } + } + + impl $crate::ironrdp_core::DecodeOwned for $pdu_ty { + fn decode_owned(src: &mut ReadCursor<'_>) -> DecodeResult { + ::decode(src) + } + } + }; +} + +/// Implements additional traits for a plain old data structure (POD). +#[macro_export] +macro_rules! impl_x224_pdu_pod { + ($pdu_ty:ty) => { + impl $crate::ironrdp_core::IntoOwned for $pdu_ty { + type Owned = Self; + + fn into_owned(self) -> Self::Owned { + self + } + } + + impl $crate::ironrdp_core::DecodeOwned for $pdu_ty { + fn decode_owned(src: &mut ReadCursor<'_>) -> DecodeResult { + <$crate::x224::X224 as $crate::ironrdp_core::Decode>::decode(src).map(|p| p.0) + } + } + }; +} + +/// Implements additional traits for a borrowing PDU and defines a static-bounded owned version. +#[macro_export] +macro_rules! impl_pdu_borrowing { + ($pdu_ty:ident $(<$($lt:lifetime),+>)?, $owned_ty:ident) => { + pub type $owned_ty = $pdu_ty<'static>; + + impl $crate::ironrdp_core::DecodeOwned for $owned_ty { + fn decode_owned(src: &mut ReadCursor<'_>) -> DecodeResult { + let pdu = <$pdu_ty $(<$($lt),+>)? as $crate::ironrdp_core::Decode>::decode(src)?; + Ok($crate::ironrdp_core::IntoOwned::into_owned(pdu)) + } + } + }; +} + +/// Implements additional traits for a borrowing PDU and defines a static-bounded owned version. +#[macro_export] +macro_rules! impl_x224_pdu_borrowing { + ($pdu_ty:ident $(<$($lt:lifetime),+>)?, $owned_ty:ident) => { + pub type $owned_ty = $pdu_ty<'static>; + + impl $crate::ironrdp_core::DecodeOwned for $owned_ty { + fn decode_owned(src: &mut ReadCursor<'_>) -> DecodeResult { + let pdu = <$crate::x224::X224<$pdu_ty $(<$($lt),+>)?> as $crate::ironrdp_core::Decode>::decode(src).map(|r| r.0)?; + Ok($crate::ironrdp_core::IntoOwned::into_owned(pdu)) + } + } + }; +} + +// FIXME: legacy macros below + +#[macro_export] +macro_rules! try_read_optional { + ($e:expr, $ret:expr) => { + match $e { + Ok(v) => v, + Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => { + return Ok($ret); + } + Err(e) => return Err(From::from(e)), + } + }; +} + +#[macro_export] +macro_rules! try_write_optional { + ($val:expr, $f:expr) => { + if let Some(ref val) = $val { + // This is a workaround for clippy false positive because + // of macro expansion. + #[expect(clippy::redundant_closure_call)] + $f(val)? + } else { + return Ok(()); + } + }; +} diff --git a/crates/ironrdp-pdu/src/mcs.rs b/crates/ironrdp-pdu/src/mcs.rs new file mode 100644 index 00000000..41ba30a6 --- /dev/null +++ b/crates/ironrdp-pdu/src/mcs.rs @@ -0,0 +1,1211 @@ +use std::borrow::Cow; + +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, other_err, read_padding, + unexpected_message_type_err, IntoOwned, ReadCursor, WriteCursor, +}; + +use crate::gcc::{ChannelDef, ClientGccBlocks, ConferenceCreateRequest, ConferenceCreateResponse}; +use crate::tpdu::{TpduCode, TpduHeader}; +use crate::tpkt::TpktHeader; +use crate::x224::{user_data_size, X224Pdu}; +use crate::{impl_x224_pdu_borrowing, impl_x224_pdu_pod, per, DecodeResult, EncodeResult, PduError}; + +// T.125 MCS is defined in: +// +// http://www.itu.int/rec/T-REC-T.125-199802-I/ +// ITU-T T.125 Multipoint Communication Service Protocol Specification +// +// Connect-Initial ::= [APPLICATION 101] IMPLICIT SEQUENCE +// { +// callingDomainSelector OCTET_STRING, +// calledDomainSelector OCTET_STRING, +// upwardFlag BOOLEAN, +// targetParameters DomainParameters, +// minimumParameters DomainParameters, +// maximumParameters DomainParameters, +// userData OCTET_STRING +// } +// +// DomainParameters ::= SEQUENCE +// { +// maxChannelIds INTEGER (0..MAX), +// maxUserIds INTEGER (0..MAX), +// maxTokenIds INTEGER (0..MAX), +// numPriorities INTEGER (0..MAX), +// minThroughput INTEGER (0..MAX), +// maxHeight INTEGER (0..MAX), +// maxMCSPDUsize INTEGER (0..MAX), +// protocolVersion INTEGER (0..MAX) +// } +// +// Connect-Response ::= [APPLICATION 102] IMPLICIT SEQUENCE +// { +// result Result, +// calledConnectId INTEGER (0..MAX), +// domainParameters DomainParameters, +// userData OCTET_STRING +// } +// +// Result ::= ENUMERATED +// { +// rt-successful (0), +// rt-domain-merging (1), +// rt-domain-not-hierarchical (2), +// rt-no-such-channel (3), +// rt-no-such-domain (4), +// rt-no-such-user (5), +// rt-not-admitted (6), +// rt-other-user-id (7), +// rt-parameters-unacceptable (8), +// rt-token-not-available (9), +// rt-token-not-possessed (10), +// rt-too-many-channels (11), +// rt-too-many-tokens (12), +// rt-too-many-users (13), +// rt-unspecified-failure (14), +// rt-user-rejected (15) +// } +// +// ErectDomainRequest ::= [APPLICATION 1] IMPLICIT SEQUENCE +// { +// subHeight INTEGER (0..MAX), +// subInterval INTEGER (0..MAX) +// } +// +// AttachUserRequest ::= [APPLICATION 10] IMPLICIT SEQUENCE +// { +// } +// +// AttachUserConfirm ::= [APPLICATION 11] IMPLICIT SEQUENCE +// { +// result Result, +// initiator UserId OPTIONAL +// } +// +// ChannelJoinRequest ::= [APPLICATION 14] IMPLICIT SEQUENCE +// { +// initiator UserId, +// channelId ChannelId +// } +// +// ChannelJoinConfirm ::= [APPLICATION 15] IMPLICIT SEQUENCE +// { +// result Result, +// initiator UserId, +// requested ChannelId, +// channelId ChannelId OPTIONAL +// } +// +// SendDataRequest ::= [APPLICATION 25] IMPLICIT SEQUENCE +// { +// initiator UserId, +// channelId ChannelId, +// dataPriority DataPriority, +// segmentation Segmentation, +// userData OCTET_STRING +// } +// +// DataPriority ::= CHOICE +// { +// top NULL, +// high NULL, +// medium NULL, +// low NULL, +// ... +// } +// +// Segmentation ::= BIT_STRING +// { +// begin (0), +// end (1) +// } (SIZE(2)) +// +// SendDataIndication ::= [APPLICATION 26] IMPLICIT SEQUENCE +// { +// initiator UserId, +// channelId ChannelId, +// dataPriority DataPriority, +// segmentation Segmentation, +// userData OCTET_STRING +// } + +pub const RESULT_ENUM_LENGTH: u8 = 16; + +const BASE_CHANNEL_ID: u16 = 1001; +const SEND_DATA_PDU_DATA_PRIORITY_AND_SEGMENTATION: u8 = 0x70; + +/// Creates a closure mapping a `PerError` to a `PduError` with field-level context. +/// +/// Shorthand for +/// ```rust +/// |e| ::invalid_field(Self::MCS_NAME, field_name, "PER").with_source(e) +/// ``` +macro_rules! per_field_err { + ($field_name:expr) => {{ + |error| ironrdp_core::invalid_field_err_with_source(Self::MCS_NAME, $field_name, "PER", error) + }}; +} + +#[doc(hidden)] +pub trait McsPdu<'de>: Sized { + const MCS_NAME: &'static str; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()>; + + fn mcs_body_decode(src: &mut ReadCursor<'de>, tpdu_user_data_size: usize) -> DecodeResult; + + fn mcs_size(&self) -> usize; + + fn name(&self) -> &'static str { + Self::MCS_NAME + } +} + +impl<'de, T> X224Pdu<'de> for T +where + T: McsPdu<'de>, +{ + const X224_NAME: &'static str = T::MCS_NAME; + + const TPDU_CODE: TpduCode = TpduCode::DATA; + + fn x224_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + self.mcs_body_encode(dst) + } + + fn x224_body_decode(src: &mut ReadCursor<'de>, tpkt: &TpktHeader, tpdu: &TpduHeader) -> DecodeResult { + let tpdu_user_data_size = user_data_size(tpkt, tpdu); + T::mcs_body_decode(src, tpdu_user_data_size) + } + + fn tpdu_header_variable_part_size(&self) -> usize { + 0 + } + + fn tpdu_user_data_size(&self) -> usize { + self.mcs_size() + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(u8)] +enum DomainMcsPdu { + ErectDomainRequest = 1, + DisconnectProviderUltimatum = 8, + AttachUserRequest = 10, + AttachUserConfirm = 11, + ChannelJoinRequest = 14, + ChannelJoinConfirm = 15, + SendDataRequest = 25, + SendDataIndication = 26, +} + +impl DomainMcsPdu { + fn check_expected(self, name: &'static str, expected: DomainMcsPdu) -> DecodeResult<()> { + if self != expected { + Err(unexpected_message_type_err!(name, self.as_u8())) + } else { + Ok(()) + } + } + + fn from_choice(choice: u8) -> Option { + Self::from_u8(choice >> 2) + } + + fn to_choice(self) -> u8 { + self.as_u8() << 2 + } + + fn from_u8(value: u8) -> Option { + match value { + 1 => Some(Self::ErectDomainRequest), + 8 => Some(Self::DisconnectProviderUltimatum), + 10 => Some(Self::AttachUserRequest), + 11 => Some(Self::AttachUserConfirm), + 14 => Some(Self::ChannelJoinRequest), + 15 => Some(Self::ChannelJoinConfirm), + 25 => Some(Self::SendDataRequest), + 26 => Some(Self::SendDataIndication), + _ => None, + } + } + + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +fn read_mcspdu_header(src: &mut ReadCursor<'_>, ctx: &'static str) -> DecodeResult { + let choice = src.try_read_u8().map_err(|e| other_err!(ctx, source: e))?; + + DomainMcsPdu::from_choice(choice) + .ok_or_else(|| invalid_field_err(ctx, "domain-mcspdu", "unexpected application tag for CHOICE")) +} + +fn peek_mcspdu_header(src: &mut ReadCursor<'_>, ctx: &'static str) -> DecodeResult { + let choice = src.try_peek_u8().map_err(|e| other_err!(ctx, source: e))?; + + DomainMcsPdu::from_choice(choice) + .ok_or_else(|| invalid_field_err(ctx, "domain-mcspdu", "unexpected application tag for CHOICE")) +} + +fn write_mcspdu_header(dst: &mut WriteCursor<'_>, domain_mcspdu: DomainMcsPdu, options: u8) { + let choice = domain_mcspdu.to_choice(); + + debug_assert_eq!(options & !0b11, 0); + debug_assert_eq!(choice & 0b11, 0); + + dst.write_u8(choice | options); +} + +/// The kind of the RDP header message that may carry additional data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum McsMessage<'a> { + ErectDomainRequest(ErectDomainPdu), + AttachUserRequest(AttachUserRequest), + AttachUserConfirm(AttachUserConfirm), + ChannelJoinRequest(ChannelJoinRequest), + ChannelJoinConfirm(ChannelJoinConfirm), + SendDataRequest(SendDataRequest<'a>), + SendDataIndication(SendDataIndication<'a>), + DisconnectProviderUltimatum(DisconnectProviderUltimatum), +} + +impl_x224_pdu_borrowing!(McsMessage<'_>, OwnedMcsMessage); + +impl IntoOwned for McsMessage<'_> { + type Owned = OwnedMcsMessage; + + fn into_owned(self) -> Self::Owned { + match self { + Self::ErectDomainRequest(msg) => McsMessage::ErectDomainRequest(msg.into_owned()), + Self::AttachUserRequest(msg) => McsMessage::AttachUserRequest(msg.into_owned()), + Self::AttachUserConfirm(msg) => McsMessage::AttachUserConfirm(msg.into_owned()), + Self::ChannelJoinRequest(msg) => McsMessage::ChannelJoinRequest(msg.into_owned()), + Self::ChannelJoinConfirm(msg) => McsMessage::ChannelJoinConfirm(msg.into_owned()), + Self::SendDataRequest(msg) => McsMessage::SendDataRequest(msg.into_owned()), + Self::SendDataIndication(msg) => McsMessage::SendDataIndication(msg.into_owned()), + Self::DisconnectProviderUltimatum(msg) => McsMessage::DisconnectProviderUltimatum(msg.into_owned()), + } + } +} + +impl<'de> McsPdu<'de> for McsMessage<'de> { + const MCS_NAME: &'static str = "McsMessage"; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + Self::ErectDomainRequest(msg) => msg.mcs_body_encode(dst), + Self::AttachUserRequest(msg) => msg.mcs_body_encode(dst), + Self::AttachUserConfirm(msg) => msg.mcs_body_encode(dst), + Self::ChannelJoinRequest(msg) => msg.mcs_body_encode(dst), + Self::ChannelJoinConfirm(msg) => msg.mcs_body_encode(dst), + Self::SendDataRequest(msg) => msg.mcs_body_encode(dst), + Self::SendDataIndication(msg) => msg.mcs_body_encode(dst), + Self::DisconnectProviderUltimatum(msg) => msg.mcs_body_encode(dst), + } + } + + fn mcs_body_decode(src: &mut ReadCursor<'de>, tpdu_user_data_size: usize) -> DecodeResult { + match peek_mcspdu_header(src, Self::MCS_NAME)? { + DomainMcsPdu::ErectDomainRequest => Ok(McsMessage::ErectDomainRequest(ErectDomainPdu::mcs_body_decode( + src, + tpdu_user_data_size, + )?)), + DomainMcsPdu::AttachUserRequest => Ok(McsMessage::AttachUserRequest(AttachUserRequest::mcs_body_decode( + src, + tpdu_user_data_size, + )?)), + DomainMcsPdu::AttachUserConfirm => Ok(McsMessage::AttachUserConfirm(AttachUserConfirm::mcs_body_decode( + src, + tpdu_user_data_size, + )?)), + DomainMcsPdu::ChannelJoinRequest => Ok(McsMessage::ChannelJoinRequest( + ChannelJoinRequest::mcs_body_decode(src, tpdu_user_data_size)?, + )), + DomainMcsPdu::ChannelJoinConfirm => Ok(McsMessage::ChannelJoinConfirm( + ChannelJoinConfirm::mcs_body_decode(src, tpdu_user_data_size)?, + )), + DomainMcsPdu::SendDataRequest => Ok(McsMessage::SendDataRequest(SendDataRequest::mcs_body_decode( + src, + tpdu_user_data_size, + )?)), + DomainMcsPdu::SendDataIndication => Ok(McsMessage::SendDataIndication( + SendDataIndication::mcs_body_decode(src, tpdu_user_data_size)?, + )), + DomainMcsPdu::DisconnectProviderUltimatum => Ok(McsMessage::DisconnectProviderUltimatum( + DisconnectProviderUltimatum::mcs_body_decode(src, tpdu_user_data_size)?, + )), + } + } + + fn mcs_size(&self) -> usize { + match self { + Self::ErectDomainRequest(msg) => msg.mcs_size(), + Self::AttachUserRequest(msg) => msg.mcs_size(), + Self::AttachUserConfirm(msg) => msg.mcs_size(), + Self::ChannelJoinRequest(msg) => msg.mcs_size(), + Self::ChannelJoinConfirm(msg) => msg.mcs_size(), + Self::SendDataRequest(msg) => msg.mcs_size(), + Self::SendDataIndication(msg) => msg.mcs_size(), + Self::DisconnectProviderUltimatum(msg) => msg.mcs_size(), + } + } + + fn name(&self) -> &'static str { + match self { + Self::ErectDomainRequest(msg) => msg.name(), + Self::AttachUserRequest(msg) => msg.name(), + Self::AttachUserConfirm(msg) => msg.name(), + Self::ChannelJoinRequest(msg) => msg.name(), + Self::ChannelJoinConfirm(msg) => msg.name(), + Self::SendDataRequest(msg) => msg.name(), + Self::SendDataIndication(msg) => msg.name(), + Self::DisconnectProviderUltimatum(msg) => msg.name(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ErectDomainPdu { + pub sub_height: u32, + pub sub_interval: u32, +} + +impl_x224_pdu_pod!(ErectDomainPdu); + +impl<'de> McsPdu<'de> for ErectDomainPdu { + const MCS_NAME: &'static str = "ErectDomainPdu"; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_mcspdu_header(dst, DomainMcsPdu::ErectDomainRequest, 0); + + per::write_u32(dst, self.sub_height); + per::write_u32(dst, self.sub_interval); + + Ok(()) + } + + fn mcs_body_decode(src: &mut ReadCursor<'de>, _: usize) -> DecodeResult { + read_mcspdu_header(src, Self::MCS_NAME)?.check_expected(Self::MCS_NAME, DomainMcsPdu::ErectDomainRequest)?; + + let sub_height = per::read_u32(src).map_err(per_field_err!("subHeight"))?; + let sub_interval = per::read_u32(src).map_err(per_field_err!("subInterval"))?; + + Ok(Self { + sub_height, + sub_interval, + }) + } + + fn mcs_size(&self) -> usize { + per::CHOICE_SIZE + per::sizeof_u32(self.sub_height) + per::sizeof_u32(self.sub_interval) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AttachUserRequest; + +impl_x224_pdu_pod!(AttachUserRequest); + +impl<'de> McsPdu<'de> for AttachUserRequest { + const MCS_NAME: &'static str = "AttachUserRequest"; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_mcspdu_header(dst, DomainMcsPdu::AttachUserRequest, 0); + + Ok(()) + } + + fn mcs_body_decode(src: &mut ReadCursor<'de>, _: usize) -> DecodeResult { + read_mcspdu_header(src, Self::MCS_NAME)?.check_expected(Self::MCS_NAME, DomainMcsPdu::AttachUserRequest)?; + + Ok(Self) + } + + fn mcs_size(&self) -> usize { + per::CHOICE_SIZE + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AttachUserConfirm { + pub result: u8, + pub initiator_id: u16, +} + +impl_x224_pdu_pod!(AttachUserConfirm); + +impl<'de> McsPdu<'de> for AttachUserConfirm { + const MCS_NAME: &'static str = "AttachUserConfirm"; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_mcspdu_header(dst, DomainMcsPdu::AttachUserConfirm, 2); + + per::write_enum(dst, self.result); + per::write_u16(dst, self.initiator_id, BASE_CHANNEL_ID).map_err(per_field_err!("initiator"))?; + + Ok(()) + } + + fn mcs_body_decode(src: &mut ReadCursor<'de>, _: usize) -> DecodeResult { + read_mcspdu_header(src, Self::MCS_NAME)?.check_expected(Self::MCS_NAME, DomainMcsPdu::AttachUserConfirm)?; + + let result = per::read_enum(src, RESULT_ENUM_LENGTH).map_err(per_field_err!("result"))?; + let user_id = per::read_u16(src, BASE_CHANNEL_ID).map_err(per_field_err!("userId"))?; + + Ok(Self { + result, + initiator_id: user_id, + }) + } + + fn mcs_size(&self) -> usize { + per::CHOICE_SIZE + per::ENUM_SIZE + per::U16_SIZE + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChannelJoinRequest { + pub initiator_id: u16, + pub channel_id: u16, +} + +impl_x224_pdu_pod!(ChannelJoinRequest); + +impl<'de> McsPdu<'de> for ChannelJoinRequest { + const MCS_NAME: &'static str = "ChannelJoinRequest"; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_mcspdu_header(dst, DomainMcsPdu::ChannelJoinRequest, 0); + + per::write_u16(dst, self.initiator_id, BASE_CHANNEL_ID).map_err(per_field_err!("initiator"))?; + per::write_u16(dst, self.channel_id, 0).map_err(per_field_err!("channelId"))?; + + Ok(()) + } + + fn mcs_body_decode(src: &mut ReadCursor<'de>, _: usize) -> DecodeResult { + read_mcspdu_header(src, Self::MCS_NAME)?.check_expected(Self::MCS_NAME, DomainMcsPdu::ChannelJoinRequest)?; + + let initiator_id = per::read_u16(src, BASE_CHANNEL_ID).map_err(per_field_err!("initiator"))?; + let channel_id = per::read_u16(src, 0).map_err(per_field_err!("channelID"))?; + + Ok(Self { + initiator_id, + channel_id, + }) + } + + fn mcs_size(&self) -> usize { + per::CHOICE_SIZE + per::U16_SIZE * 2 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChannelJoinConfirm { + pub result: u8, + pub initiator_id: u16, + pub requested_channel_id: u16, + pub channel_id: u16, +} + +impl_x224_pdu_pod!(ChannelJoinConfirm); + +impl<'de> McsPdu<'de> for ChannelJoinConfirm { + const MCS_NAME: &'static str = "ChannelJoinConfirm"; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_mcspdu_header(dst, DomainMcsPdu::ChannelJoinConfirm, 2); + + per::write_enum(dst, self.result); + per::write_u16(dst, self.initiator_id, BASE_CHANNEL_ID).map_err(per_field_err!("initiator"))?; + per::write_u16(dst, self.requested_channel_id, 0).map_err(per_field_err!("requested"))?; + per::write_u16(dst, self.channel_id, 0).map_err(per_field_err!("channelId"))?; + + Ok(()) + } + + fn mcs_body_decode(src: &mut ReadCursor<'de>, _: usize) -> DecodeResult { + read_mcspdu_header(src, Self::MCS_NAME)?.check_expected(Self::MCS_NAME, DomainMcsPdu::ChannelJoinConfirm)?; + + let result = per::read_enum(src, RESULT_ENUM_LENGTH).map_err(per_field_err!("result"))?; + let initiator_id = per::read_u16(src, BASE_CHANNEL_ID).map_err(per_field_err!("initiator"))?; + let requested_channel_id = per::read_u16(src, 0).map_err(per_field_err!("requested"))?; + let channel_id = per::read_u16(src, 0).map_err(per_field_err!("channelId"))?; + + Ok(Self { + result, + initiator_id, + requested_channel_id, + channel_id, + }) + } + + fn mcs_size(&self) -> usize { + per::CHOICE_SIZE + per::ENUM_SIZE + per::U16_SIZE * 3 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SendDataRequest<'a> { + pub initiator_id: u16, + pub channel_id: u16, + pub user_data: Cow<'a, [u8]>, +} + +impl_x224_pdu_borrowing!(SendDataRequest<'_>, OwnedSendDataRequest); + +impl IntoOwned for SendDataRequest<'_> { + type Owned = OwnedSendDataRequest; + + fn into_owned(self) -> Self::Owned { + SendDataRequest { + user_data: Cow::Owned(self.user_data.into_owned()), + ..self + } + } +} + +impl<'de> McsPdu<'de> for SendDataRequest<'de> { + const MCS_NAME: &'static str = "SendDataRequest"; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_mcspdu_header(dst, DomainMcsPdu::SendDataRequest, 0); + + per::write_u16(dst, self.initiator_id, BASE_CHANNEL_ID).map_err(per_field_err!("initiator"))?; + per::write_u16(dst, self.channel_id, 0).map_err(per_field_err!("channelID"))?; + + dst.write_u8(SEND_DATA_PDU_DATA_PRIORITY_AND_SEGMENTATION); + + per::write_length(dst, cast_length!("user-data-length", self.user_data.len())?); + dst.write_slice(&self.user_data); + + Ok(()) + } + + fn mcs_body_decode(src: &mut ReadCursor<'de>, tpdu_user_data_size: usize) -> DecodeResult { + let src_len_before = src.len(); + + read_mcspdu_header(src, Self::MCS_NAME)?.check_expected(Self::MCS_NAME, DomainMcsPdu::SendDataRequest)?; + + let initiator_id = per::read_u16(src, BASE_CHANNEL_ID).map_err(per_field_err!("initiator"))?; + let channel_id = per::read_u16(src, 0).map_err(per_field_err!("channelId"))?; + + // dataPriority + segmentation + ensure_size!(ctx: Self::MCS_NAME, in: src, size: 1); + read_padding!(src, 1); + + let (length, _) = per::read_length(src).map_err(per_field_err!("userDataLength"))?; + let length = usize::from(length); + + let src_len_after = src.len(); + + if length > tpdu_user_data_size.saturating_sub(src_len_before - src_len_after) { + return Err(invalid_field_err( + Self::MCS_NAME, + "userDataLength", + "inconsistent with user data size advertised in TPDU", + )); + } + + ensure_size!(ctx: Self::MCS_NAME, in: src, size: length); + let user_data = Cow::Borrowed(src.read_slice(length)); + + Ok(Self { + initiator_id, + channel_id, + user_data, + }) + } + + fn mcs_size(&self) -> usize { + per::CHOICE_SIZE + per::U16_SIZE * 2 + 1 + per::sizeof_length(self.user_data.len()) + self.user_data.len() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SendDataIndication<'a> { + pub initiator_id: u16, + pub channel_id: u16, + pub user_data: Cow<'a, [u8]>, +} + +impl_x224_pdu_borrowing!(SendDataIndication<'_>, OwnedSendDataIndication); + +impl IntoOwned for SendDataIndication<'_> { + type Owned = OwnedSendDataIndication; + + fn into_owned(self) -> Self::Owned { + SendDataIndication { + user_data: Cow::Owned(self.user_data.into_owned()), + ..self + } + } +} + +impl<'de> McsPdu<'de> for SendDataIndication<'de> { + const MCS_NAME: &'static str = "SendDataIndication"; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_mcspdu_header(dst, DomainMcsPdu::SendDataIndication, 0); + + per::write_u16(dst, self.initiator_id, BASE_CHANNEL_ID).map_err(per_field_err!("initiator"))?; + per::write_u16(dst, self.channel_id, 0).map_err(per_field_err!("channelId"))?; + + dst.write_u8(SEND_DATA_PDU_DATA_PRIORITY_AND_SEGMENTATION); + + per::write_length(dst, cast_length!("userDataLength", self.user_data.len())?); + dst.write_slice(&self.user_data); + + Ok(()) + } + + fn mcs_body_decode(src: &mut ReadCursor<'de>, tpdu_user_data_size: usize) -> DecodeResult { + let src_len_before = src.len(); + + read_mcspdu_header(src, Self::MCS_NAME)?.check_expected(Self::MCS_NAME, DomainMcsPdu::SendDataIndication)?; + + let initiator_id = per::read_u16(src, BASE_CHANNEL_ID).map_err(per_field_err!("initiator"))?; + let channel_id = per::read_u16(src, 0).map_err(per_field_err!("channelId"))?; + + // dataPriority + segmentation + ensure_size!(ctx: Self::MCS_NAME, in: src, size: 1); + read_padding!(src, 1); + + let (length, _) = per::read_length(src).map_err(per_field_err!("userDataLength"))?; + let length = usize::from(length); + + let src_len_after = src.len(); + + if length > tpdu_user_data_size.saturating_sub(src_len_before - src_len_after) { + return Err(invalid_field_err( + Self::MCS_NAME, + "userDataLength", + "inconsistent with user data size advertised in TPDU", + )); + } + + ensure_size!(ctx: Self::MCS_NAME, in: src, size: length); + let user_data = Cow::Borrowed(src.read_slice(length)); + + Ok(Self { + initiator_id, + channel_id, + user_data, + }) + } + + fn mcs_size(&self) -> usize { + per::CHOICE_SIZE + per::U16_SIZE * 2 + 1 + per::sizeof_length(self.user_data.len()) + self.user_data.len() + } +} + +/// The reason of `DisconnectProviderUltimatum`. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(u8)] +pub enum DisconnectReason { + DomainDisconnected = 0, + ProviderInitiated = 1, + TokenPurged = 2, + UserRequested = 3, + ChannelPurged = 4, +} + +impl DisconnectReason { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } + + pub fn from_u8(value: u8) -> Option { + match value { + 0 => Some(Self::DomainDisconnected), + 1 => Some(Self::ProviderInitiated), + 2 => Some(Self::TokenPurged), + 3 => Some(Self::UserRequested), + 4 => Some(Self::ChannelPurged), + _ => None, + } + } + + pub fn description(self) -> &'static str { + match self { + Self::DomainDisconnected => "domain disconnected", + Self::ProviderInitiated => "server-initiated disconnect", + Self::TokenPurged => "token purged", + Self::UserRequested => "user-requested disconnect", + Self::ChannelPurged => "channel purged", + } + } +} + +impl core::fmt::Display for DisconnectReason { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.description()) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct DisconnectProviderUltimatum { + pub reason: DisconnectReason, +} + +impl_x224_pdu_pod!(DisconnectProviderUltimatum); + +impl DisconnectProviderUltimatum { + pub const NAME: &'static str = "DisconnectProviderUltimatum"; + + pub const FIXED_PART_SIZE: usize = 2; + + pub fn from_reason(reason: DisconnectReason) -> Self { + Self { reason } + } +} + +impl<'de> McsPdu<'de> for DisconnectProviderUltimatum { + const MCS_NAME: &'static str = "DisconnectProviderUltimatum"; + + fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let domain_mcspdu = DomainMcsPdu::DisconnectProviderUltimatum.as_u8(); + let reason = self.reason.as_u8(); + + let b1 = (domain_mcspdu << 2) | ((reason >> 1) & 0x03); + let b2 = reason << 7; + + dst.write_array([b1, b2]); + + Ok(()) + } + + fn mcs_body_decode(src: &mut ReadCursor<'de>, _: usize) -> DecodeResult { + // http://msdn.microsoft.com/en-us/library/cc240872.aspx: + // + // PER encoded (ALIGNED variant of BASIC-PER) PDU contents: + // 21 80 + // + // 0x21: + // 0 - --\ + // 0 - | + // 1 - | CHOICE: From DomainMCSPDU select disconnectProviderUltimatum (8) + // 0 - | of type DisconnectProviderUltimatum + // 0 - | + // 0 - --/ + // 0 - --\ + // 1 - | + // | DisconnectProviderUltimatum::reason = rn-user-requested (3) + // 0x80: | + // 1 - --/ + // 0 - padding + // 0 - padding + // 0 - padding + // 0 - padding + // 0 - padding + // 0 - padding + // 0 - padding + + ensure_fixed_part_size!(in: src); + + let [b1, b2] = src.read_array(); + + let domain_mcspdu_choice = b1 >> 2; + let reason = ((b1 & 0x03) << 1) | (b2 >> 7); + + DomainMcsPdu::from_u8(domain_mcspdu_choice) + .ok_or_else(|| invalid_field_err(Self::MCS_NAME, "domain-mcspdu", "unexpected application tag for CHOICE"))? + .check_expected(Self::MCS_NAME, DomainMcsPdu::DisconnectProviderUltimatum)?; + + Ok(Self { + reason: DisconnectReason::from_u8(reason) + .ok_or_else(|| invalid_field_err(Self::MCS_NAME, "reason", "unknown variant"))?, + }) + } + + fn mcs_size(&self) -> usize { + 2 + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ConnectInitial { + pub conference_create_request: ConferenceCreateRequest, + pub calling_domain_selector: Vec, + pub called_domain_selector: Vec, + pub upward_flag: bool, + pub target_parameters: DomainParameters, + pub min_parameters: DomainParameters, + pub max_parameters: DomainParameters, +} + +impl ConnectInitial { + pub fn with_gcc_blocks(gcc_blocks: ClientGccBlocks) -> DecodeResult { + Ok(Self { + conference_create_request: ConferenceCreateRequest::new(gcc_blocks)?, + calling_domain_selector: vec![0x01], + called_domain_selector: vec![0x01], + upward_flag: true, + target_parameters: DomainParameters::target(), + min_parameters: DomainParameters::min(), + max_parameters: DomainParameters::max(), + }) + } + + pub fn channel_names(&self) -> Option> { + self.conference_create_request.gcc_blocks().channel_names() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ConnectResponse { + pub conference_create_response: ConferenceCreateResponse, + pub called_connect_id: u32, + pub domain_parameters: DomainParameters, +} + +impl ConnectResponse { + pub fn channel_ids(&self) -> Vec { + self.conference_create_response.gcc_blocks().channel_ids() + } + + pub fn global_channel_id(&self) -> u16 { + self.conference_create_response.gcc_blocks().global_channel_id() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DomainParameters { + pub max_channel_ids: u32, + pub max_user_ids: u32, + pub max_token_ids: u32, + pub num_priorities: u32, + pub min_throughput: u32, + pub max_height: u32, + pub max_mcs_pdu_size: u32, + pub protocol_version: u32, +} + +impl DomainParameters { + pub fn min() -> Self { + Self { + max_channel_ids: 1, + max_user_ids: 1, + max_token_ids: 1, + num_priorities: 1, + min_throughput: 0, + max_height: 1, + max_mcs_pdu_size: 1056, + protocol_version: 2, + } + } + + pub fn target() -> Self { + Self { + max_channel_ids: 34, + max_user_ids: 2, + max_token_ids: 0, + num_priorities: 1, + min_throughput: 0, + max_height: 1, + max_mcs_pdu_size: 65535, + protocol_version: 2, + } + } + + pub fn max() -> Self { + Self { + max_channel_ids: 65535, + max_user_ids: 64535, + max_token_ids: 65535, + num_priorities: 1, + min_throughput: 0, + max_height: 1, + max_mcs_pdu_size: 65535, + protocol_version: 2, + } + } +} + +pub use legacy::McsError; + +mod legacy { + #![allow( + clippy::multiple_inherent_impl, + reason = "Cannot move the implementation from the legacy module" + )] + + use std::io; + + use ironrdp_core::{cast_int, Decode, DecodeResult, Encode}; + use thiserror::Error; + + use super::{ + cast_length, ensure_size, ConnectInitial, ConnectResponse, DomainParameters, PduError, ReadCursor, WriteCursor, + RESULT_ENUM_LENGTH, + }; + use crate::gcc::{ConferenceCreateRequest, ConferenceCreateResponse, GccError}; + use crate::{ber, EncodeResult}; + + // impl<'de> McsPdu<'de> for ConnectInitial { + // const MCS_NAME: &'static str = "DisconnectProviderUltimatum"; + + // fn mcs_body_encode(&self, dst: &mut WriteCursor<'_>) -> Result<()> { + // todo!() + // } + + // fn mcs_body_decode(src: &mut ReadCursor<'de>, tpdu_user_data_size: usize) -> Result { + // todo!() + // } + + // fn mcs_size(&self) -> usize { + // todo!() + // } + // } + + const MCS_TYPE_CONNECT_INITIAL: u8 = 0x65; + const MCS_TYPE_CONNECT_RESPONSE: u8 = 0x66; + + impl ConnectInitial { + const NAME: &'static str = "ConnectInitial"; + + fn fields_buffer_ber_length(&self) -> usize { + // Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error. + #[expect(clippy::cast_possible_truncation, clippy::as_conversions)] + { + ber::sizeof_octet_string(self.calling_domain_selector.len() as u16) + + ber::sizeof_octet_string(self.called_domain_selector.len() as u16) + + ber::SIZEOF_BOOL + + (self.target_parameters.size() + self.min_parameters.size() + self.max_parameters.size()) + + ber::sizeof_octet_string(self.conference_create_request.size() as u16) + } + } + } + + impl Encode for ConnectInitial { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let field_buffer_ber_length = cast_length!("field_buffer_ber_length", self.fields_buffer_ber_length())?; + ber::write_application_tag(dst, MCS_TYPE_CONNECT_INITIAL, field_buffer_ber_length)?; + ber::write_octet_string(dst, self.calling_domain_selector.as_ref())?; + ber::write_octet_string(dst, self.called_domain_selector.as_ref())?; + ber::write_bool(dst, self.upward_flag)?; + self.target_parameters.encode(dst)?; + self.min_parameters.encode(dst)?; + self.max_parameters.encode(dst)?; + ber::write_octet_string_tag(dst, cast_length!("len", self.conference_create_request.size())?)?; + self.conference_create_request.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let fields_buffer_ber_length = self.fields_buffer_ber_length(); + // Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error. + #[expect(clippy::cast_possible_truncation, clippy::as_conversions)] + let fields_buffer_ber_length_u16 = fields_buffer_ber_length as u16; + + fields_buffer_ber_length + + ber::sizeof_application_tag(MCS_TYPE_CONNECT_INITIAL, fields_buffer_ber_length_u16) + } + } + + impl<'de> Decode<'de> for ConnectInitial { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ber::read_application_tag(src, MCS_TYPE_CONNECT_INITIAL)?; + let calling_domain_selector = ber::read_octet_string(src)?; + let called_domain_selector = ber::read_octet_string(src)?; + let upward_flag = ber::read_bool(src)?; + let target_parameters = DomainParameters::decode(src)?; + let min_parameters = DomainParameters::decode(src)?; + let max_parameters = DomainParameters::decode(src)?; + let _user_data_buffer_length = ber::read_octet_string_tag(src)?; + let conference_create_request = ConferenceCreateRequest::decode(src)?; + + Ok(Self { + conference_create_request, + calling_domain_selector, + called_domain_selector, + upward_flag, + target_parameters, + min_parameters, + max_parameters, + }) + } + } + + impl ConnectResponse { + const NAME: &'static str = "ConnectResponse"; + + fn fields_buffer_ber_length(&self) -> usize { + // Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error. + #[expect(clippy::cast_possible_truncation, clippy::as_conversions)] + { + ber::SIZEOF_ENUMERATED + + ber::sizeof_integer(self.called_connect_id) + + self.domain_parameters.size() + + ber::sizeof_octet_string(self.conference_create_response.size() as u16) + } + } + } + + impl Encode for ConnectResponse { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let field_buffer_ber_length = cast_length!("field_buffer_ber_length", self.fields_buffer_ber_length())?; + ber::write_application_tag(dst, MCS_TYPE_CONNECT_RESPONSE, field_buffer_ber_length)?; + ber::write_enumerated(dst, 0)?; + ber::write_integer(dst, self.called_connect_id)?; + self.domain_parameters.encode(dst)?; + ber::write_octet_string_tag(dst, cast_length!("len", self.conference_create_response.size())?)?; + self.conference_create_response.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let fields_buffer_ber_length = self.fields_buffer_ber_length(); + // Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error. + #[expect(clippy::cast_possible_truncation, clippy::as_conversions)] + let fields_buffer_ber_length_u16 = fields_buffer_ber_length as u16; + fields_buffer_ber_length + + ber::sizeof_application_tag(MCS_TYPE_CONNECT_RESPONSE, fields_buffer_ber_length_u16) + } + } + + impl<'de> Decode<'de> for ConnectResponse { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ber::read_application_tag(src, MCS_TYPE_CONNECT_RESPONSE)?; + ber::read_enumerated(src, RESULT_ENUM_LENGTH)?; + let called_connect_id = cast_int!("called_connect_id", ber::read_integer(src)?)?; + let domain_parameters = DomainParameters::decode(src)?; + let _user_data_buffer_length = ber::read_octet_string_tag(src)?; + let conference_create_response = ConferenceCreateResponse::decode(src)?; + + Ok(Self { + called_connect_id, + domain_parameters, + conference_create_response, + }) + } + } + + impl DomainParameters { + const NAME: &'static str = "DomainParameters"; + + fn fields_buffer_ber_length(&self) -> usize { + ber::sizeof_integer(self.max_channel_ids) + + ber::sizeof_integer(self.max_user_ids) + + ber::sizeof_integer(self.max_token_ids) + + ber::sizeof_integer(self.num_priorities) + + ber::sizeof_integer(self.min_throughput) + + ber::sizeof_integer(self.max_height) + + ber::sizeof_integer(self.max_mcs_pdu_size) + + ber::sizeof_integer(self.protocol_version) + } + } + + impl Encode for DomainParameters { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + ber::write_sequence_tag(dst, cast_length!("seqTagLen", self.fields_buffer_ber_length())?)?; + ber::write_integer(dst, self.max_channel_ids)?; + ber::write_integer(dst, self.max_user_ids)?; + ber::write_integer(dst, self.max_token_ids)?; + ber::write_integer(dst, self.num_priorities)?; + ber::write_integer(dst, self.min_throughput)?; + ber::write_integer(dst, self.max_height)?; + ber::write_integer(dst, self.max_mcs_pdu_size)?; + ber::write_integer(dst, self.protocol_version)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let fields_buffer_ber_length = self.fields_buffer_ber_length(); + + // Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error. + #[expect(clippy::cast_possible_truncation, clippy::as_conversions)] + let fields_buffer_ber_length_u16 = fields_buffer_ber_length as u16; + // FIXME: maybe size should return PduResult... + fields_buffer_ber_length + ber::sizeof_sequence_tag(fields_buffer_ber_length_u16) + } + } + + impl<'de> Decode<'de> for DomainParameters { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ber::read_sequence_tag(src)?; + let max_channel_ids = cast_int!("max_channel_ids", ber::read_integer(src)?)?; + let max_user_ids = cast_int!("max_user_ids", ber::read_integer(src)?)?; + let max_token_ids = cast_int!("max_token_ids", ber::read_integer(src)?)?; + let num_priorities = cast_int!("num_priorities", ber::read_integer(src)?)?; + let min_throughput = cast_int!("min_throughput", ber::read_integer(src)?)?; + let max_height = cast_int!("max_height", ber::read_integer(src)?)?; + let max_mcs_pdu_size = cast_int!("max_mcs_pdu_size", ber::read_integer(src)?)?; + let protocol_version = cast_int!("protocol_version", ber::read_integer(src)?)?; + + Ok(Self { + max_channel_ids, + max_user_ids, + max_token_ids, + num_priorities, + min_throughput, + max_height, + max_mcs_pdu_size, + protocol_version, + }) + } + } + + #[derive(Debug, Error)] + pub enum McsError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("GCC block error")] + GccError(#[from] GccError), + #[error("invalid disconnect provider ultimatum")] + InvalidDisconnectProviderUltimatum, + #[error("invalid domain MCS PDU")] + InvalidDomainMcsPdu, + #[error("invalid MCS Connection Sequence PDU")] + InvalidPdu(String), + #[error("invalid invalid MCS channel id")] + UnexpectedChannelId(String), + #[error("PDU error: {0}")] + Pdu(PduError), + } + + impl From for McsError { + fn from(e: PduError) -> Self { + Self::Pdu(e) + } + } + + impl From for io::Error { + fn from(e: McsError) -> io::Error { + io::Error::other(format!("MCS Connection Sequence error: {e}")) + } + } +} diff --git a/crates/ironrdp-pdu/src/nego.rs b/crates/ironrdp-pdu/src/nego.rs new file mode 100644 index 00000000..abfe6f00 --- /dev/null +++ b/crates/ironrdp-pdu/src/nego.rs @@ -0,0 +1,485 @@ +//! PDUs used during the Connection Initiation stage + +use core::fmt; + +use bitflags::bitflags; +use ironrdp_core::{ensure_size, invalid_field_err, unexpected_message_type_err, ReadCursor, WriteCursor}; +use tap::prelude::*; + +use crate::tpdu::{TpduCode, TpduHeader}; +use crate::tpkt::TpktHeader; +use crate::x224::X224Pdu; +use crate::{impl_x224_pdu_pod, DecodeResult, EncodeResult, Pdu as _}; + +bitflags! { + /// A 32-bit, unsigned integer that contains flags indicating the supported security protocols. + /// + /// Used to negotiate the security protocol to use during the Connection Initiation phase using + /// the [`ConnectionConfirm`] and [`ConnectionRequest`] messages. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct SecurityProtocol: u32 { + /// PROTOCOL_SSL, TLS + login subsystem (winlogon.exe) + const SSL = 0x0000_0001; + /// PROTOCOL_HYBRID, TLS + Credential Security Support Provider protocol (CredSSP) + const HYBRID = 0x0000_0002; + /// PROTOCOL_RDSTLS, RDSTLS protocol + const RDSTLS = 0x0000_0004; + /// PROTOCOL_HYBRID_EX, TLS + Credential Security Support Provider protocol (CredSSP) coupled with the Early User Authorization Result PDU + const HYBRID_EX = 0x0000_0008; + /// PROTOCOL_RDSAAD, RDS-AAD-Auth Security + const RDSAAD = 0x0000_0010; + } +} + +impl SecurityProtocol { + /// Returns true if no enhanced security protocol is enabled + /// + /// The PROTOCOL_RDP bitmask is defined as 0x00000000. + /// Hence, this is logically equivalent to `SecurityProtocol::is_empty()`, but more explicit in the intention. + /// + /// As a server, to convey that the standard RDP security protocol has been chosen, no flag must be set. + /// As a client, the standard RDP security is always implied because there is no flag to set or unset. + pub fn is_standard_rdp_security(self) -> bool { + self.is_empty() + } +} + +impl fmt::Display for SecurityProtocol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_standard_rdp_security() { + write!(f, "STANDARD_RDP_SECURITY") + } else { + bitflags::parser::to_writer(self, f) + } + } +} + +bitflags! { + /// Holds the negotiation protocol flags of the *request* message. + /// + /// # MSDN + /// + /// * [RDP Negotiation Request (RDP_NEG_REQ)](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/902b090b-9cb3-4efc-92bf-ee13373371e3) + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct RequestFlags: u8 { + const RESTRICTED_ADMIN_MODE_REQUIRED = 0x01; + const REDIRECTED_AUTHENTICATION_MODE_REQUIRED = 0x02; + const CORRELATION_INFO_PRESENT = 0x08; + } +} + +bitflags! { + /// Holds the negotiation protocol flags of the *response* message. + /// + /// # MSDN + /// + /// * [RDP Negotiation Response (RDP_NEG_RSP)](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/b2975bdc-6d56-49ee-9c57-f2ff3a0b6817) + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ResponseFlags: u8 { + const EXTENDED_CLIENT_DATA_SUPPORTED = 0x01; + const DYNVC_GFX_PROTOCOL_SUPPORTED = 0x02; + const RDP_NEG_RSP_RESERVED = 0x04; + const RESTRICTED_ADMIN_MODE_SUPPORTED = 0x08; + const REDIRECTED_AUTHENTICATION_MODE_SUPPORTED = 0x10; + } +} + +/// A 32-bit, unsigned integer that specifies the negotiation failure code +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct FailureCode(u32); + +impl FailureCode { + /// The server requires that the client support Enhanced RDP Security (section 5.4) + /// with either TLS 1.0, 1.1 or 1.2 (section 5.4.5.1) or CredSSP (section 5.4.5.2). + /// If only CredSSP was requested then the server only supports TLS. + pub const SSL_REQUIRED_BY_SERVER: Self = Self(1); + /// The server is configured to only use Standard RDP Security mechanisms (section + /// 5.3) and does not support any External Security Protocols (section 5.4.5). + pub const SSL_NOT_ALLOWED_BY_SERVER: Self = Self(2); + /// The server does not possess a valid authentication certificate and cannot + /// initialize the External Security Protocol Provider (section 5.4.5). + pub const SSL_CERT_NOT_ON_SERVER: Self = Self(3); + /// The list of requested security protocols is not consistent with the current + /// security protocol in effect. This error is only possible when the Direct + /// Approach (sections 5.4.2.2 and 1.3.1.2) is used and an External Security + /// Protocol (section 5.4.5) is already being used. + pub const INCONSISTENT_FLAGS: Self = Self(4); + /// The server requires that the client support Enhanced RDP Security (section 5.4) + /// with CredSSP (section 5.4.5.2). + pub const HYBRID_REQUIRED_BY_SERVER: Self = Self(5); + /// The server requires that the client support Enhanced RDP Security (section + /// 5.4) with TLS 1.0, 1.1 or 1.2 (section 5.4.5.1) and certificate-based client + /// authentication. + pub const SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER: Self = Self(6); +} + +impl From for FailureCode { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(value: FailureCode) -> Self { + value.0 + } +} + +impl fmt::Display for FailureCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::SSL_REQUIRED_BY_SERVER => { + write!(f, "enhanced RDP security required by server") + } + Self::SSL_NOT_ALLOWED_BY_SERVER => { + write!(f, "enhanced RDP security not allowed by server") + } + Self::SSL_CERT_NOT_ON_SERVER => { + write!(f, "no valid TLS authentication certificate on server") + } + Self::INCONSISTENT_FLAGS => { + write!(f, "inconsistent flags for security protocols") + } + Self::HYBRID_REQUIRED_BY_SERVER => { + write!(f, "CredSSP enhanced RDP security required by server") + } + Self::SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER => { + write!(f, "TLS certificate-based client authentication required by server") + } + _ => write!(f, "unknown failure code: {}", self.0), + } + } +} + +/// The kind of the negotiation request message. +/// +/// # MSDN +/// +/// * [Client X.224 Connection Request PDU](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/18a27ef9-6f9a-4501-b000-94b1fe3c2c10) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NegoRequestData { + RoutingToken(RoutingToken), + Cookie(Cookie), +} + +impl NegoRequestData { + pub fn routing_token(value: String) -> Self { + Self::RoutingToken(RoutingToken(value)) + } + + pub fn cookie(value: String) -> Self { + Self::Cookie(Cookie(value)) + } + + pub fn read(src: &mut ReadCursor<'_>) -> DecodeResult> { + match RoutingToken::read(src)? { + Some(token) => Ok(Some(Self::RoutingToken(token))), + None => Cookie::read(src)?.map(Self::Cookie).pipe(Ok), + } + } + + pub fn write(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + NegoRequestData::RoutingToken(token) => token.write(dst), + NegoRequestData::Cookie(cookie) => cookie.write(dst), + } + } + + pub fn size(&self) -> usize { + match self { + NegoRequestData::RoutingToken(token) => token.size(), + NegoRequestData::Cookie(cookie) => cookie.size(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Cookie(pub String); + +impl Cookie { + const PREFIX: &'static str = "Cookie: mstshash="; + + pub fn read(src: &mut ReadCursor<'_>) -> DecodeResult> { + read_nego_data(src, "Cookie", Self::PREFIX)?.map(Self).pipe(Ok) + } + + pub fn write(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_nego_data(dst, "Cookie", Self::PREFIX, &self.0) + } + + pub fn size(&self) -> usize { + Self::PREFIX.len() + self.0.len() + 2 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RoutingToken(pub String); + +impl RoutingToken { + const PREFIX: &'static str = "Cookie: msts="; + + pub fn read(src: &mut ReadCursor<'_>) -> DecodeResult> { + read_nego_data(src, "RoutingToken", Self::PREFIX)?.map(Self).pipe(Ok) + } + + pub fn write(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_nego_data(dst, "RoutingToken", Self::PREFIX, &self.0) + } + + pub fn size(&self) -> usize { + Self::PREFIX.len() + self.0.len() + 2 + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct NegoMsgType(u8); + +impl NegoMsgType { + const REQUEST: Self = Self(0x01); + const RESPONSE: Self = Self(0x02); + const FAILURE: Self = Self(0x03); +} + +impl From for NegoMsgType { + fn from(value: u8) -> Self { + Self(value) + } +} + +impl From for u8 { + fn from(value: NegoMsgType) -> Self { + value.0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConnectionRequest { + pub nego_data: Option, + pub flags: RequestFlags, + pub protocol: SecurityProtocol, +} + +impl_x224_pdu_pod!(ConnectionRequest); + +impl ConnectionRequest { + const RDP_NEG_REQ_SIZE: u16 = 8; +} + +impl<'de> X224Pdu<'de> for ConnectionRequest { + const X224_NAME: &'static str = "Client X.224 Connection Request"; + + const TPDU_CODE: TpduCode = TpduCode::CONNECTION_REQUEST; + + fn x224_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + if let Some(nego_data) = &self.nego_data { + nego_data.write(dst)?; + } + + // [MS-RDPBCGR] mentions the following payload as optional, but it appears that on recent + // versions of Windows, the server always expect to find this payload. + dst.write_u8(u8::from(NegoMsgType::REQUEST)); + dst.write_u8(self.flags.bits()); + dst.write_u16(Self::RDP_NEG_REQ_SIZE); + dst.write_u32(self.protocol.bits()); + + if self.flags.contains(RequestFlags::CORRELATION_INFO_PRESENT) { + // TODO(#111): support for RDP_NEG_CORRELATION_INFO + return Err(invalid_field_err( + Self::NAME, + "flags", + "CORRECTION_INFO_PRESENT flag is set, but not supported by IronRDP", + )); + } + + Ok(()) + } + + fn x224_body_decode(src: &mut ReadCursor<'de>, _: &TpktHeader, tpdu: &TpduHeader) -> DecodeResult { + let variable_part_size = tpdu.variable_part_size(); + + ensure_size!(in: src, size: variable_part_size); + + let nego_data = NegoRequestData::read(src)?; + + let Some(variable_part_rest_size) = + variable_part_size.checked_sub(nego_data.as_ref().map(|data| data.size()).unwrap_or(0)) + else { + return Err(invalid_field_err( + Self::NAME, + "TPDU header variable part", + "advertised size too small", + )); + }; + + if variable_part_rest_size >= usize::from(Self::RDP_NEG_REQ_SIZE) { + let msg_type = NegoMsgType::from(src.read_u8()); + + if msg_type != NegoMsgType::REQUEST { + return Err(unexpected_message_type_err!(Self::NAME, u8::from(msg_type))); + } + + let flags = RequestFlags::from_bits_truncate(src.read_u8()); + + if flags.contains(RequestFlags::CORRELATION_INFO_PRESENT) { + // TODO(#111): support for RDP_NEG_CORRELATION_INFO + return Err(invalid_field_err( + Self::NAME, + "flags", + "CORRECTION_INFO_PRESENT flag is set, but not supported by IronRDP", + )); + } + + let _length = src.read_u16(); + + let protocol = SecurityProtocol::from_bits_truncate(src.read_u32()); + + Ok(Self { + nego_data, + flags, + protocol, + }) + } else { + Ok(Self { + nego_data, + flags: RequestFlags::empty(), + protocol: SecurityProtocol::empty(), + }) + } + } + + fn tpdu_header_variable_part_size(&self) -> usize { + let optional_nego_data_size = self.nego_data.as_ref().map(|data| data.size()).unwrap_or(0); + optional_nego_data_size + usize::from(Self::RDP_NEG_REQ_SIZE) + } + + fn tpdu_user_data_size(&self) -> usize { + 0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConnectionConfirm { + Response { + flags: ResponseFlags, + protocol: SecurityProtocol, + }, + Failure { + code: FailureCode, + }, +} + +impl_x224_pdu_pod!(ConnectionConfirm); + +impl ConnectionConfirm { + const RDP_NEG_RSP: u16 = 8; + + const RDP_NEG_FAILURE: u16 = 8; +} + +impl<'de> X224Pdu<'de> for ConnectionConfirm { + const X224_NAME: &'static str = "Server X.224 Connection Confirm"; + + const TPDU_CODE: TpduCode = TpduCode::CONNECTION_CONFIRM; + + fn x224_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + ConnectionConfirm::Response { flags, protocol } => { + dst.write_u8(u8::from(NegoMsgType::RESPONSE)); + dst.write_u8(flags.bits()); + dst.write_u16(Self::RDP_NEG_RSP); + dst.write_u32(protocol.bits()); + } + ConnectionConfirm::Failure { code } => { + dst.write_u8(u8::from(NegoMsgType::FAILURE)); + dst.write_u8(0); + dst.write_u16(Self::RDP_NEG_RSP); + dst.write_u32(u32::from(*code)); + } + } + + Ok(()) + } + + fn x224_body_decode(src: &mut ReadCursor<'de>, _: &TpktHeader, tpdu: &TpduHeader) -> DecodeResult { + let variable_part_size = tpdu.variable_part_size(); + + ensure_size!(in: src, size: variable_part_size); + + if variable_part_size > 0 { + ensure_size!(in: src, size: 8); // message type (1) + flags (1) + length (2) + code / protocol (4) + + match NegoMsgType::from(src.read_u8()) { + NegoMsgType::RESPONSE => { + let flags = ResponseFlags::from_bits_truncate(src.read_u8()); + let _length = src.read_u16(); + let protocol = SecurityProtocol::from_bits_truncate(src.read_u32()); + + Ok(Self::Response { flags, protocol }) + } + NegoMsgType::FAILURE => { + let _flags = src.read_u8(); + let _length = src.read_u16(); + let code = FailureCode::from(src.read_u32()); + + Ok(Self::Failure { code }) + } + unexpected => Err(unexpected_message_type_err!(Self::X224_NAME, u8::from(unexpected))), + } + } else { + Ok(Self::Response { + flags: ResponseFlags::empty(), + protocol: SecurityProtocol::empty(), + }) + } + } + + fn tpdu_header_variable_part_size(&self) -> usize { + match self { + ConnectionConfirm::Response { .. } => usize::from(Self::RDP_NEG_RSP), + ConnectionConfirm::Failure { .. } => usize::from(Self::RDP_NEG_FAILURE), + } + } + + fn tpdu_user_data_size(&self) -> usize { + 0 + } +} + +fn read_nego_data(src: &mut ReadCursor<'_>, ctx: &'static str, prefix: &str) -> DecodeResult> { + if src.len() < prefix.len() + 2 { + return Ok(None); + } + + if src.peek_slice(prefix.len()) != prefix.as_bytes() { + return Ok(None); + } + + src.advance(prefix.len()); + + let identifier_start = src.pos(); + + while src.peek_u16() != 0x0A0D { + src.advance(1); + ensure_size!(ctx: ctx, in: src, size: 2); + } + + let identifier_end = src.pos(); + + src.advance(2); + + let data = core::str::from_utf8(&src.inner()[identifier_start..identifier_end]) + .map_err(|_| invalid_field_err(ctx, "identifier", "not valid UTF-8"))? + .to_owned(); + + Ok(Some(data)) +} + +fn write_nego_data(dst: &mut WriteCursor<'_>, ctx: &'static str, prefix: &str, value: &str) -> EncodeResult<()> { + ensure_size!(ctx: ctx, in: dst, size: prefix.len() + value.len() + 2); + + dst.write_slice(prefix.as_bytes()); + dst.write_slice(value.as_bytes()); + dst.write_u16(0x0A0D); + + Ok(()) +} diff --git a/crates/ironrdp-pdu/src/pcb.rs b/crates/ironrdp-pdu/src/pcb.rs new file mode 100644 index 00000000..687a4650 --- /dev/null +++ b/crates/ironrdp-pdu/src/pcb.rs @@ -0,0 +1,158 @@ +//! This module contains the RDP_PRECONNECTION_PDU_V1 and RDP_PRECONNECTION_PDU_V2 structures. + +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, invalid_field_err_with_source, read_padding, + write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; + +use crate::Pdu; + +/// Preconnection PDU version +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PcbVersion(pub u32); + +impl PcbVersion { + pub const V1: Self = Self(0x1); + pub const V2: Self = Self(0x2); +} + +/// RDP preconnection PDU +/// +/// The RDP_PRECONNECTION_PDU_V1 is used by the client to let the listening process +/// know which RDP source the connection is intended for. +/// +/// The RDP_PRECONNECTION_PDU_V2 extends the RDP_PRECONNECTION_PDU_V1 packet by +/// adding a variable-size Unicode character string. The receiver of this PDU can +/// use this string and the Id field of the RDP_PRECONNECTION_PDU_V1 packet to +/// determine the RDP source. This string is opaque to the protocol. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreconnectionBlob { + /// Preconnection PDU version + pub version: PcbVersion, + /// This field is used to uniquely identify the RDP source. Although the Id can be + /// as simple as a process ID, it is often client-specific or server-specific and + /// can be obfuscated. + pub id: u32, + /// V2 PCB string + pub v2_payload: Option, +} + +impl PreconnectionBlob { + pub const FIXED_PART_SIZE: usize = 16; +} + +impl Pdu for PreconnectionBlob { + const NAME: &'static str = "PreconnectionBlob"; +} + +impl<'de> Decode<'de> for PreconnectionBlob { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let pcb_size: usize = cast_length!("cbSize", src.read_u32())?; + + if pcb_size < Self::FIXED_PART_SIZE { + return Err(invalid_field_err( + Self::NAME, + "cbSize", + "advertised size too small for Preconnection PDU V1", + )); + } + + read_padding!(src, 4); // flags + + // The version field SHOULD be initialized by the client and SHOULD be ignored by the server, + // as specified in sections 3.1.5.1 and 3.2.5.1. + // That’s why, following code doesn’t depend on the value of this field. + let version = PcbVersion(src.read_u32()); + + let id = src.read_u32(); + + let remaining_size = pcb_size - Self::FIXED_PART_SIZE; + + ensure_size!(in: src, size: remaining_size); + + if remaining_size >= 2 { + let cch_pcb = usize::from(src.read_u16()); + let cb_pcb = cch_pcb * 2; + + if remaining_size - 2 < cb_pcb { + return Err(invalid_field_err( + Self::NAME, + "cchPCB", + "PCB string bigger than advertised size", + )); + } + + let wsz_pcb_utf16 = src.read_slice(cb_pcb); + + let payload = crate::utf16::read_utf16_string(wsz_pcb_utf16, Some(cch_pcb)) + .map_err(|e| invalid_field_err_with_source(Self::NAME, "wszPCB", "bad UTF-16 string", e))?; + + let leftover_size = remaining_size - 2 - cb_pcb; + src.advance(leftover_size); // Consume (unused) leftover data + + Ok(Self { + version, + id, + v2_payload: Some(payload), + }) + } else { + Ok(Self { + version, + id, + v2_payload: None, + }) + } + } +} + +impl Encode for PreconnectionBlob { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + if self.v2_payload.is_some() && self.version == PcbVersion::V1 { + return Err(invalid_field_err( + Self::NAME, + "version", + "there is no string payload in Preconnection PDU V1", + )); + } + + let pcb_size = self.size(); + + ensure_size!(in: dst, size: pcb_size); + + dst.write_u32(cast_length!("cbSize", pcb_size)?); // cbSize + write_padding!(dst, 4); // flags + dst.write_u32(self.version.0); // version + dst.write_u32(self.id); // id + + if let Some(v2_payload) = &self.v2_payload { + // cchPCB + let utf16_character_count = v2_payload.chars().count() + 1; // +1 for null terminator + dst.write_u16(cast_length!("cchPCB", utf16_character_count)?); + + // wszPCB + v2_payload.encode_utf16().for_each(|c| dst.write_u16(c)); + dst.write_u16(0); // null terminator + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let fixed_part_size = Self::FIXED_PART_SIZE; + + let variable_part = if let Some(v2_payload) = &self.v2_payload { + let utf16_encoded_len = crate::utf16::null_terminated_utf16_encoded_len(v2_payload); + 2 + utf16_encoded_len + } else { + 0 + }; + + fixed_part_size + variable_part + } +} diff --git a/crates/ironrdp-pdu/src/per.rs b/crates/ironrdp-pdu/src/per.rs new file mode 100644 index 00000000..5538a6e2 --- /dev/null +++ b/crates/ironrdp-pdu/src/per.rs @@ -0,0 +1,876 @@ +#![allow(dead_code)] + +use core::fmt; + +use ironrdp_core::{ReadCursor, WriteCursor}; + +pub(crate) const CHOICE_SIZE: usize = 1; +pub(crate) const ENUM_SIZE: usize = 1; +pub(crate) const U16_SIZE: usize = 2; + +const OBJECT_ID_SIZE: usize = 6; + +#[derive(Clone, Debug)] +pub(crate) enum PerError { + NotEnoughBytes { available: usize, required: usize }, + InvalidLength { reason: &'static str }, + Overflow, + Underflow, + UnexpectedEnumVariant, + OctetStringTooSmall, + OctetStringTooBig, + NumericStringTooSmall, + NumericStringTooBig, +} + +impl core::error::Error for PerError {} + +impl fmt::Display for PerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PerError::NotEnoughBytes { available, required } => write!( + f, + "not enough bytes to read PEM element: {available} bytes availables, required {required} bytes" + ), + PerError::InvalidLength { reason } => write!(f, "invalid length: {reason}"), + PerError::Overflow => write!(f, "overflow"), + PerError::Underflow => write!(f, "underflow"), + PerError::UnexpectedEnumVariant => write!(f, "enumerated value does not fall within the expected range"), + PerError::OctetStringTooSmall => write!(f, "octet string too small"), + PerError::OctetStringTooBig => write!(f, "octet string too big"), + PerError::NumericStringTooSmall => write!(f, "numeric string too small"), + PerError::NumericStringTooBig => write!(f, "numeric string too big"), + } + } +} + +fn try_read_u8(src: &mut ReadCursor<'_>) -> Result { + if src.is_empty() { + Err(PerError::NotEnoughBytes { + available: src.len(), + required: 1, + }) + } else { + Ok(src.read_u8()) + } +} + +fn try_read_u16_be(src: &mut ReadCursor<'_>) -> Result { + if src.len() >= 2 { + Ok(src.read_u16_be()) + } else { + Err(PerError::NotEnoughBytes { + available: src.len(), + required: 2, + }) + } +} + +fn try_read_u32_be(src: &mut ReadCursor<'_>) -> Result { + if src.len() >= 4 { + Ok(src.read_u32_be()) + } else { + Err(PerError::NotEnoughBytes { + available: src.len(), + required: 4, + }) + } +} + +fn try_read_slice<'a>(src: &mut ReadCursor<'a>, n: usize) -> Result<&'a [u8], PerError> { + if src.len() >= n { + Ok(src.read_slice(n)) + } else { + Err(PerError::NotEnoughBytes { + available: src.len(), + required: n, + }) + } +} + +pub(crate) fn read_length(src: &mut ReadCursor<'_>) -> Result<(u16, usize), PerError> { + let a = try_read_u8(src)?; + + if a & 0x80 != 0 { + let b = try_read_u8(src)?; + let length = ((u16::from(a) & !0x80) << 8) + u16::from(b); + + Ok((length, 2)) + } else { + Ok((u16::from(a), 1)) + } +} + +pub(crate) fn write_length(dst: &mut WriteCursor<'_>, length: u16) { + if length > 0x7f { + write_long_length(dst, length); + } else { + dst.write_u8(u8::try_from(length).expect("length is guaranteed to fit into u8 due to the prior check")); + } +} + +/// Force write length as 2 bytes even if it is less than 0x80 +pub(crate) fn write_long_length(dst: &mut WriteCursor<'_>, length: u16) { + dst.write_u16_be(length | 0x8000) +} + +pub(crate) fn sizeof_length(length: usize) -> usize { + if length > 0x7f { + 2 + } else { + 1 + } +} + +pub(crate) fn sizeof_long_length() -> usize { + 2 +} + +pub(crate) fn sizeof_u32(value: u32) -> usize { + if value <= 0xff { + 2 + } else if value <= 0xffff { + 3 + } else { + 5 + } +} + +pub(crate) fn read_choice(src: &mut ReadCursor<'_>) -> u8 { + src.read_u8() +} + +pub(crate) fn write_choice(dst: &mut WriteCursor<'_>, choice: u8) { + dst.write_u8(choice); +} + +pub(crate) fn read_selection(src: &mut ReadCursor<'_>) -> u8 { + src.read_u8() +} + +pub(crate) fn write_selection(dst: &mut WriteCursor<'_>, selection: u8) { + dst.write_u8(selection); +} + +pub(crate) fn read_number_of_sets(src: &mut ReadCursor<'_>) -> u8 { + src.read_u8() +} + +pub(crate) fn write_number_of_sets(dst: &mut WriteCursor<'_>, number_of_sets: u8) { + dst.write_u8(number_of_sets); +} + +pub(crate) fn read_padding(src: &mut ReadCursor<'_>, padding_length: usize) { + src.advance(padding_length); +} + +pub(crate) fn write_padding(dst: &mut WriteCursor<'_>, padding_length: usize) { + for _ in 0..padding_length { + dst.write_u8(0); + } +} + +pub(crate) fn read_u32(src: &mut ReadCursor<'_>) -> Result { + let (length, _) = read_length(src)?; + + match length { + 0 => Ok(0), + 1 => Ok(u32::from(try_read_u8(src)?)), + 2 => Ok(u32::from(try_read_u16_be(src)?)), + 4 => Ok(try_read_u32_be(src)?), + _ => Err(PerError::InvalidLength { + reason: "U32 with length greater than 4 bytes", + }), + } +} + +pub(crate) fn write_u32(dst: &mut WriteCursor<'_>, value: u32) { + if value <= 0xff { + write_length(dst, 1); + dst.write_u8(u8::try_from(value).expect("value is guaranteed to fit into u8 due to the prior check")); + } else if value <= 0xffff { + write_length(dst, 2); + dst.write_u16_be(u16::try_from(value).expect("value is guaranteed to fit into u16 due to the prior check")); + } else { + write_length(dst, 4); + dst.write_u32_be(value); + } +} + +pub(crate) fn read_u16(src: &mut ReadCursor<'_>, min: u16) -> Result { + let value = try_read_u16_be(src)?; + min.checked_add(value).ok_or(PerError::Overflow) +} + +pub(crate) fn write_u16(dst: &mut WriteCursor<'_>, value: u16, min: u16) -> Result<(), PerError> { + dst.write_u16_be(value.checked_sub(min).ok_or(PerError::Underflow)?); + Ok(()) +} + +pub(crate) fn read_enum(src: &mut ReadCursor<'_>, count: u8) -> Result { + let enumerated = try_read_u8(src)?; + + if enumerated >= count { + Err(PerError::UnexpectedEnumVariant) + } else { + Ok(enumerated) + } +} + +pub(crate) fn write_enum(dst: &mut WriteCursor<'_>, enumerated: u8) { + dst.write_u8(enumerated); +} + +pub(crate) fn read_object_id(src: &mut ReadCursor<'_>) -> Result<[u8; OBJECT_ID_SIZE], PerError> { + let (length, _) = read_length(src)?; + + if length != 5 { + return Err(PerError::InvalidLength { + reason: "invalid OID length advertised", + }); + } + + let first_two_tuples = try_read_u8(src)?; + + let mut read_object_ids = [0u8; OBJECT_ID_SIZE]; + read_object_ids[0] = first_two_tuples / 40; + read_object_ids[1] = first_two_tuples % 40; + for read_object_id in read_object_ids.iter_mut().skip(2) { + *read_object_id = try_read_u8(src)?; + } + + Ok(read_object_ids) +} + +pub(crate) fn write_object_id(dst: &mut WriteCursor<'_>, object_ids: [u8; OBJECT_ID_SIZE]) { + write_length( + dst, + u16::try_from(OBJECT_ID_SIZE).expect("OBJECT_ID_SIZE fits into u16") - 1, + ); + + let first_two_tuples = object_ids[0] * 40 + object_ids[1]; + dst.write_u8(first_two_tuples); + + for object_id in object_ids.iter().skip(2) { + dst.write_u8(*object_id); + } +} + +pub(crate) fn read_octet_string<'a>(src: &mut ReadCursor<'a>, min: usize) -> Result<&'a [u8], PerError> { + let (length, _) = read_length(src)?; + let read_len = min + usize::from(length); + let octet_string = try_read_slice(src, read_len)?; + Ok(octet_string) +} + +pub(crate) fn write_octet_string(dst: &mut WriteCursor<'_>, octet_string: &[u8], min: usize) -> Result<(), PerError> { + if octet_string.len() < min { + return Err(PerError::OctetStringTooSmall); + } + + let length = octet_string.len() - min; + let length = u16::try_from(length).map_err(|_| PerError::OctetStringTooBig)?; + write_length(dst, length); + + dst.write_slice(octet_string); + + Ok(()) +} + +pub(crate) fn read_numeric_string(src: &mut ReadCursor<'_>, min: u16) -> Result<(), PerError> { + let (length, _) = read_length(src)?; + let length = usize::from((length + min).div_ceil(2)); + + if src.len() < length { + Err(PerError::NotEnoughBytes { + available: src.len(), + required: length, + }) + } else { + src.advance(length); + Ok(()) + } +} + +pub(crate) fn write_numeric_string(dst: &mut WriteCursor<'_>, num_str: &[u8], min: usize) -> Result<(), PerError> { + if num_str.len() < min { + return Err(PerError::NumericStringTooSmall); + } + + let length = num_str.len() - min; + let length = u16::try_from(length).map_err(|_| PerError::NumericStringTooBig)?; + + write_length(dst, length); + + let magic_transform = |elem| (elem - 0x30) % 10; + + for pair in num_str.chunks(2) { + let first = magic_transform(pair[0]); + let second = magic_transform(if pair.len() == 1 { 0x30 } else { pair[1] }); + + let num = (first << 4) | second; + + dst.write_u8(num); + } + + Ok(()) +} + +pub(crate) mod legacy { + use std::io; + + use byteorder::{BigEndian, ReadBytesExt as _, WriteBytesExt as _}; + + use super::OBJECT_ID_SIZE; + + pub(crate) fn read_length(mut stream: impl io::Read) -> io::Result<(u16, usize)> { + let a = stream.read_u8()?; + + if a & 0x80 != 0 { + let b = stream.read_u8()?; + let length = ((u16::from(a) & !0x80) << 8) + u16::from(b); + + Ok((length, 2)) + } else { + Ok((u16::from(a), 1)) + } + } + + pub(crate) fn write_long_length(mut stream: impl io::Write, length: u16) -> io::Result { + stream.write_u16::(length | 0x8000)?; + Ok(2) + } + + pub(crate) fn write_short_length(mut stream: impl io::Write, length: u8) -> io::Result { + stream.write_u8(length)?; + Ok(1) + } + + pub(crate) fn write_length(stream: impl io::Write, length: u16) -> io::Result { + if length > 0x7f { + write_long_length(stream, length) + } else { + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "cast is valid due to prior check" + )] + let length = length as u8; + write_short_length(stream, length) + } + } + + pub(crate) fn read_choice(mut stream: impl io::Read) -> io::Result { + stream.read_u8() + } + + pub(crate) fn write_choice(mut stream: impl io::Write, choice: u8) -> io::Result { + stream.write_u8(choice)?; + + Ok(1) + } + + pub(crate) fn read_selection(mut stream: impl io::Read) -> io::Result { + stream.read_u8() + } + + pub(crate) fn write_selection(mut stream: impl io::Write, selection: u8) -> io::Result { + stream.write_u8(selection)?; + + Ok(1) + } + + pub(crate) fn read_number_of_sets(mut stream: impl io::Read) -> io::Result { + stream.read_u8() + } + + pub(crate) fn write_number_of_sets(mut stream: impl io::Write, number_of_sets: u8) -> io::Result { + stream.write_u8(number_of_sets)?; + + Ok(1) + } + + pub(crate) fn read_padding(mut stream: impl io::Read, padding_length: usize) -> io::Result<()> { + let mut buf = vec![0; padding_length]; + stream.read_exact(buf.as_mut())?; + + Ok(()) + } + + pub(crate) fn write_padding(mut stream: impl io::Write, padding_length: usize) -> io::Result<()> { + let buf = vec![0; padding_length]; + stream.write_all(buf.as_ref())?; + + Ok(()) + } + + pub(crate) fn read_u32(mut stream: impl io::Read) -> io::Result { + let (length, _) = read_length(&mut stream)?; + + match length { + 0 => Ok(0), + 1 => Ok(u32::from(stream.read_u8()?)), + 2 => Ok(u32::from(stream.read_u16::()?)), + 4 => stream.read_u32::(), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid PER length: {length}"), + )), + } + } + + pub(crate) fn write_u32(mut stream: impl io::Write, value: u32) -> io::Result { + if value <= 0xff { + let size = write_length(&mut stream, 1)?; + + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "cast is valid due to prior check" + )] + let value = value as u8; + stream.write_u8(value)?; + + Ok(size + 1) + } else if value <= 0xffff { + let size = write_length(&mut stream, 2)?; + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "cast is valid due to prior check" + )] + let value = value as u16; + stream.write_u16::(value)?; + + Ok(size + 2) + } else { + let size = write_length(&mut stream, 4)?; + stream.write_u32::(value)?; + + Ok(size + 4) + } + } + + pub(crate) fn read_u16(mut stream: impl io::Read, min: u16) -> io::Result { + min.checked_add(stream.read_u16::()?) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid PER u16")) + } + + pub(crate) fn write_u16(mut stream: impl io::Write, value: u16, min: u16) -> io::Result { + if value < min { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Min is greater then number", + )) + } else { + stream.write_u16::(value - min)?; + + Ok(2) + } + } + + pub(crate) fn read_enum(mut stream: impl io::Read, count: u8) -> io::Result { + let enumerated = stream.read_u8()?; + + if u16::from(enumerated) + 1 > u16::from(count) { + Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Enumerated value ({enumerated}) does not fall within expected range"), + )) + } else { + Ok(enumerated) + } + } + + pub(crate) fn write_enum(mut stream: impl io::Write, enumerated: u8) -> io::Result { + stream.write_u8(enumerated)?; + + Ok(1) + } + + pub(crate) fn read_object_id(mut stream: impl io::Read) -> io::Result<[u8; OBJECT_ID_SIZE]> { + let (length, _) = read_length(&mut stream)?; + if length != 5 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "invalid PER object id length", + )); + } + + let first_two_tuples = stream.read_u8()?; + + let mut read_object_ids = [0u8; OBJECT_ID_SIZE]; + read_object_ids[0] = first_two_tuples / 40; + read_object_ids[1] = first_two_tuples % 40; + for read_object_id in read_object_ids.iter_mut().skip(2) { + *read_object_id = stream.read_u8()?; + } + + Ok(read_object_ids) + } + + pub(crate) fn write_object_id(mut stream: impl io::Write, object_ids: [u8; OBJECT_ID_SIZE]) -> io::Result { + let object_oid_size: u16 = OBJECT_ID_SIZE + .try_into() + .expect("OBJECT_ID_SIZE is known to fit into u16"); + let size = write_length(&mut stream, object_oid_size - 1)?; + + let first_two_tuples = object_ids[0] * 40 + object_ids[1]; + stream.write_u8(first_two_tuples)?; + + for object_id in object_ids.iter().skip(2) { + stream.write_u8(*object_id)?; + } + + Ok(size + OBJECT_ID_SIZE - 1) + } + + pub(crate) fn read_octet_string(mut stream: impl io::Read, min: usize) -> io::Result> { + let (read_length, _) = read_length(&mut stream)?; + + let mut read_octet_string = vec![0; min + usize::from(read_length)]; + stream.read_exact(read_octet_string.as_mut())?; + + Ok(read_octet_string) + } + + pub(crate) fn write_octet_string(mut stream: impl io::Write, octet_string: &[u8], min: usize) -> io::Result { + let length = if octet_string.len() >= min { + octet_string.len() - min + } else { + min + }; + + let length = u16::try_from(length) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid octet string length"))?; + let size = write_length(&mut stream, length)?; + stream.write_all(octet_string)?; + + Ok(size + octet_string.len()) + } + + pub(crate) fn read_numeric_string(mut stream: impl io::Read, min: u16) -> io::Result<()> { + let (read_length, _) = read_length(&mut stream)?; + + let length = (read_length + min).div_ceil(2); + + let mut read_numeric_string = vec![0; usize::from(length)]; + stream.read_exact(read_numeric_string.as_mut())?; + + Ok(()) + } + + pub(crate) fn write_numeric_string(mut stream: impl io::Write, num_str: &[u8], min: usize) -> io::Result { + let length = if num_str.len() >= min { num_str.len() - min } else { min }; + + let length = u16::try_from(length) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid numeric string length"))?; + let mut size = write_length(&mut stream, length)?; + + let magic_transform = |elem| (elem - 0x30) % 10; + + for pair in num_str.chunks(2) { + let first = magic_transform(pair[0]); + let second = magic_transform(if pair.len() == 1 { 0x30 } else { pair[1] }); + + let num = (first << 4) | second; + + stream.write_u8(num)?; + size += 1; + } + + Ok(size) + } +} + +#[cfg(test)] +#[expect( + clippy::needless_raw_strings, + reason = "the lint is disable to not interfere with expect! macro" +)] +mod tests { + use expect_test::expect; + + use super::*; + + #[test] + fn read_length_is_correct_length() { + let mut src = ReadCursor::new(&[0x05]); + + let (length, sizeof_length) = read_length(&mut src).unwrap(); + + assert_eq!(5, length); + assert_eq!(src.len(), 0); + assert_eq!(sizeof_length, 1); + } + + #[test] + fn read_length_is_correct_long_length() { + let mut src = ReadCursor::new(&[0x80, 0x8d]); + + let (length, sizeof_length) = read_length(&mut src).unwrap(); + + assert_eq!(141, length); + assert_eq!(src.len(), 0); + assert_eq!(sizeof_length, 2); + } + + #[test] + fn write_length_is_correct() { + let expected_buf = [0x05]; + + let mut buf = [0; 1]; + let mut dst = WriteCursor::new(&mut buf); + write_length(&mut dst, 0x05); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } + + #[test] + fn write_length_is_correct_with_long_length() { + let expected_buf = [0x80, 0x8d]; + + let mut buf = [0; 2]; + let mut dst = WriteCursor::new(&mut buf); + write_length(&mut dst, 141); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } + + #[test] + fn sizeof_length_is_correct_with_small_length() { + assert_eq!(1, sizeof_length(10)); + } + + #[test] + fn sizeof_length_is_correct_with_long_length() { + assert_eq!(2, sizeof_length(10_000)); + } + + #[test] + fn read_u32_returns_correct_with_null_number() { + let buf = [0x00]; + let mut src = ReadCursor::new(&buf); + assert_eq!(0, read_u32(&mut src).unwrap()); + } + + #[test] + fn read_u32_returns_correct_with_1_byte_number() { + let buf = [0x01, 0x7f]; + let mut src = ReadCursor::new(&buf); + assert_eq!(127, read_u32(&mut src).unwrap()); + } + + #[test] + fn read_u32_returns_correct_with_2_bytes_number() { + let buf = [0x02, 0x7f, 0xff]; + let mut src = ReadCursor::new(&buf); + assert_eq!(32767, read_u32(&mut src).unwrap()); + } + + #[test] + fn read_u32_returns_correct_with_4_bytes_number() { + let buf = [0x04, 0x01, 0x12, 0xA8, 0x80]; + let mut src = ReadCursor::new(&buf); + assert_eq!(18_000_000, read_u32(&mut src).unwrap()); + } + + #[test] + fn read_u32_fails_on_invalid_length() { + let buf = [0x03, 0x01, 0x12, 0xA8, 0x80]; + let mut src = ReadCursor::new(&buf); + assert!(read_u32(&mut src).is_err()); + } + + #[test] + fn write_u32_returns_correct_null_number() { + let expected_buf = [0x01, 0x00]; + + let mut buf = [0; 2]; + let mut dst = WriteCursor::new(&mut buf); + write_u32(&mut dst, 0); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } + + #[test] + fn write_u32_returns_correct_1_byte_number() { + let expected_buf = [0x01, 0x7f]; + + let mut buf = [0; 2]; + let mut dst = WriteCursor::new(&mut buf); + write_u32(&mut dst, 127); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } + + #[test] + fn write_u32_returns_correct_2_bytes_number() { + let expected_buf = [0x02, 0x7f, 0xff]; + + let mut buf = [0; 3]; + let mut dst = WriteCursor::new(&mut buf); + write_u32(&mut dst, 32767); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } + + #[test] + fn write_u32_returns_correct_4_byte_number() { + let expected_buf = [0x04, 0x01, 0x12, 0xA8, 0x80]; + + let mut buf = [0; 5]; + let mut dst = WriteCursor::new(&mut buf); + write_u32(&mut dst, 18_000_000); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } + + #[test] + fn read_u16_returns_correct_number() { + let buf = [0x00, 0x07]; + let mut src = ReadCursor::new(&buf); + assert_eq!(1008, read_u16(&mut src, 1001).unwrap()); + } + + #[test] + fn read_u16_fails_on_too_big_number_with_min_value() { + let buf = [0xff, 0xff]; + let mut src = ReadCursor::new(&buf); + + let e = read_u16(&mut src, 1).err().unwrap(); + + expect![[r#" + Overflow + "#]] + .assert_debug_eq(&e) + } + + #[test] + fn write_u16_returns_correct_number() { + let expected_buf = [0x00, 0x07]; + + let mut buf = [0; 2]; + let mut dst = WriteCursor::new(&mut buf); + write_u16(&mut dst, 1008, 1001).unwrap(); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } + + #[test] + fn write_u16_fails_if_min_is_greater_then_number() { + let mut buf = [0; 2]; + let mut dst = WriteCursor::new(&mut buf); + + let e = write_u16(&mut dst, 1000, 1001).err().unwrap(); + + expect![[r#" + Underflow + "#]] + .assert_debug_eq(&e); + } + + #[test] + fn read_object_id_returns_ok() { + let buf = [0x05, 0x00, 0x14, 0x7c, 0x00, 0x01]; + let mut src = ReadCursor::new(&buf); + assert_eq!([0, 0, 20, 124, 0, 1], read_object_id(&mut src).unwrap()); + } + + #[test] + fn write_object_id_is_correct() { + let expected_buf = [0x05, 0x00, 0x14, 0x7c, 0x00, 0x01]; + + let mut buf = [0; 6]; + let mut dst = WriteCursor::new(&mut buf); + write_object_id(&mut dst, [0, 0, 20, 124, 0, 1]); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } + + #[test] + fn read_enum_fails_on_invalid_enum_with_count() { + let buf = [0x05]; + let mut src = ReadCursor::new(&buf); + + let e = read_enum(&mut src, 1).err().unwrap(); + + expect![[r#" + UnexpectedEnumVariant + "#]] + .assert_debug_eq(&e); + } + + #[test] + fn read_enum_returns_correct_enum() { + let buf = [0x05]; + let mut src = ReadCursor::new(&buf); + + assert_eq!(5, read_enum(&mut src, 10).unwrap()); + } + + #[test] + fn read_enum_fails_on_max_number() { + let buf = [0xff]; + let mut src = ReadCursor::new(&buf); + + let e = read_enum(&mut src, 0xff).err().unwrap(); + + expect![[r#" + UnexpectedEnumVariant + "#]] + .assert_debug_eq(&e); + } + + #[test] + fn read_numeric_string_no_panic() { + let buf = [0x00, 0x10]; + let mut src = ReadCursor::new(&buf); + + read_numeric_string(&mut src, 1).unwrap(); + } + + #[test] + fn write_numeric_string_is_correct() { + let expected_buf = [0x00, 0x10]; + let octet_string = b"1"; + + let mut buf = [0; 2]; + let mut dst = WriteCursor::new(&mut buf); + + write_numeric_string(&mut dst, octet_string, 1).unwrap(); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } + + #[test] + fn read_octet_string_returns_ok() { + let buf = [0x00, 0x44, 0x75, 0x63, 0x61]; + let mut src = ReadCursor::new(&buf); + + assert_eq!(b"Duca", read_octet_string(&mut src, 4).unwrap()); + } + + #[test] + fn write_octet_string_is_correct() { + let expected_buf = [0x00, 0x44, 0x75, 0x63, 0x61]; + let octet_string = b"Duca"; + + let mut buf = [0; 5]; + let mut dst = WriteCursor::new(&mut buf); + + write_octet_string(&mut dst, octet_string, 4).unwrap(); + + assert_eq!(dst.len(), 0); + assert_eq!(buf, expected_buf); + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap.rs new file mode 100644 index 00000000..cd5a4f4c --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap.rs @@ -0,0 +1,108 @@ +#[cfg(test)] +mod tests; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, read_padding, write_padding, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; + +const BITMAP_LENGTH: usize = 24; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct BitmapDrawingFlags: u8 { + const ALLOW_DYNAMIC_COLOR_FIDELITY = 0x02; + const ALLOW_COLOR_SUBSAMPLING = 0x04; + const ALLOW_SKIP_ALPHA = 0x08; + const UNUSED_FLAG = 0x10; + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Bitmap { + pub pref_bits_per_pix: u16, + pub desktop_width: u16, + pub desktop_height: u16, + pub desktop_resize_flag: bool, + pub drawing_flags: BitmapDrawingFlags, +} + +impl Bitmap { + const NAME: &'static str = "Bitmap"; + + const FIXED_PART_SIZE: usize = BITMAP_LENGTH; +} + +impl Encode for Bitmap { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.pref_bits_per_pix); + dst.write_u16(1); // receive1BitPerPixel + dst.write_u16(1); // receive4BitsPerPixel + dst.write_u16(1); // receive8BitsPerPixel + dst.write_u16(self.desktop_width); + dst.write_u16(self.desktop_height); + write_padding!(dst, 2); + dst.write_u16(u16::from(self.desktop_resize_flag)); + dst.write_u16(1); // bitmapCompressionFlag + dst.write_u8(0); // highColorFlags + dst.write_u8(self.drawing_flags.bits()); + dst.write_u16(1); // multipleRectangleSupport + write_padding!(dst, 2); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Bitmap { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let pref_bits_per_pix = src.read_u16(); + + let _receive_1_bit_per_pixel = src.read_u16() != 0; + let _receive_4_bit_per_pixel = src.read_u16() != 0; + let _receive_8_bit_per_pixel = src.read_u16() != 0; + let desktop_width = src.read_u16(); + let desktop_height = src.read_u16(); + read_padding!(src, 2); + let desktop_resize_flag = src.read_u16() != 0; + + let is_bitmap_compress_flag_set = src.read_u16() != 0; + if !is_bitmap_compress_flag_set { + return Err(invalid_field_err!( + "isBitmapCompressFlagSet", + "invalid compression flag" + )); + } + + let _high_color_flags = src.read_u8(); + let drawing_flags = BitmapDrawingFlags::from_bits_truncate(src.read_u8()); + + // According to the spec: + // "This field MUST be set to TRUE (0x0001) because multiple rectangle support is required for a connection to proceed." + // however like FreeRDP, we will ignore this field. + // https://github.com/FreeRDP/FreeRDP/blob/ba8cf8cf2158018fb7abbedb51ab245f369be813/libfreerdp/core/capabilities.c#L391 + let _ = src.read_u16(); + + read_padding!(src, 2); + + Ok(Bitmap { + pref_bits_per_pix, + desktop_width, + desktop_height, + desktop_resize_flag, + drawing_flags, + }) + } +} diff --git a/ironrdp/src/rdp/capability_sets/bitmap/test.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap/tests.rs similarity index 62% rename from ironrdp/src/rdp/capability_sets/bitmap/test.rs rename to crates/ironrdp-pdu/src/rdp/capability_sets/bitmap/tests.rs index 454a17ff..78b8fa10 100644 --- a/ironrdp/src/rdp/capability_sets/bitmap/test.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap/tests.rs @@ -1,4 +1,6 @@ -use lazy_static::lazy_static; +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; use super::*; @@ -18,30 +20,25 @@ const BITMAP_BUFFER: [u8; 24] = [ 0x00, 0x00, // pad2octetsB ]; -lazy_static! { - pub static ref BITMAP: Bitmap = Bitmap { - pref_bits_per_pix: 24, - desktop_width: 1280, - desktop_height: 1024, - desktop_resize_flag: true, - drawing_flags: BitmapDrawingFlags::ALLOW_SKIP_ALPHA, - }; -} +static BITMAP: LazyLock = LazyLock::new(|| Bitmap { + pref_bits_per_pix: 24, + desktop_width: 1280, + desktop_height: 1024, + desktop_resize_flag: true, + drawing_flags: BitmapDrawingFlags::ALLOW_SKIP_ALPHA, +}); #[test] fn from_buffer_correctly_parses_bitmap_capset() { let buffer = BITMAP_BUFFER.as_ref(); - assert_eq!(*BITMAP, Bitmap::from_buffer(buffer).unwrap()); + let bitmap = LazyLock::force(&BITMAP); + assert_eq!(bitmap, &decode(buffer).unwrap()); } #[test] fn to_buffer_correctly_serializes_bitmap_capset() { - let mut buffer = Vec::new(); - - let capset = &BITMAP; - - capset.to_buffer(&mut buffer).unwrap(); + let buffer = encode_vec(LazyLock::force(&BITMAP)).unwrap(); assert_eq!(buffer, BITMAP_BUFFER.as_ref()); } @@ -50,5 +47,5 @@ fn to_buffer_correctly_serializes_bitmap_capset() { fn buffer_length_is_correct_for_bitmap_capset() { let correct_buffer_length = BITMAP_BUFFER.len(); - assert_eq!(correct_buffer_length, BITMAP.buffer_length()); + assert_eq!(correct_buffer_length, BITMAP.size()); } diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_cache/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_cache/mod.rs new file mode 100644 index 00000000..76c53866 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_cache/mod.rs @@ -0,0 +1,230 @@ +#[cfg(test)] +mod tests; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, read_padding, write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +pub const BITMAP_CACHE_ENTRIES_NUM: usize = 3; + +const BITMAP_CACHE_LENGTH: usize = 36; +const BITMAP_CACHE_REV2_LENGTH: usize = 36; +const CELL_INFO_LENGTH: usize = 4; +const BITMAP_CACHE_REV2_CELL_INFO_NUM: usize = 5; +const CACHE_ENTRY_LENGTH: usize = 4; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct BitmapCache { + pub caches: [CacheEntry; BITMAP_CACHE_ENTRIES_NUM], +} + +impl BitmapCache { + const NAME: &'static str = "BitmapCache"; + + const FIXED_PART_SIZE: usize = BITMAP_CACHE_LENGTH; +} + +impl Encode for BitmapCache { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + write_padding!(dst, 24); + + for cache in self.caches.iter() { + cache.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for BitmapCache { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + read_padding!(src, 24); + + let mut caches = [CacheEntry::default(); BITMAP_CACHE_ENTRIES_NUM]; + + for cache in caches.iter_mut() { + *cache = CacheEntry::decode(src)?; + } + + Ok(BitmapCache { caches }) + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)] +pub struct CacheEntry { + pub entries: u16, + pub max_cell_size: u16, +} + +impl CacheEntry { + const NAME: &'static str = "CacheEntry"; + + const FIXED_PART_SIZE: usize = CACHE_ENTRY_LENGTH; +} + +impl Encode for CacheEntry { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.entries); + dst.write_u16(self.max_cell_size); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for CacheEntry { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let entries = src.read_u16(); + let max_cell_size = src.read_u16(); + + Ok(CacheEntry { entries, max_cell_size }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CacheFlags: u16 { + const PERSISTENT_KEYS_EXPECTED_FLAG = 1; + const ALLOW_CACHE_WAITING_LIST_FLAG = 2; + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct BitmapCacheRev2 { + pub cache_flags: CacheFlags, + pub num_cell_caches: u8, + pub cache_cell_info: [CellInfo; BITMAP_CACHE_REV2_CELL_INFO_NUM], +} + +impl BitmapCacheRev2 { + const NAME: &'static str = "BitmapCacheRev2"; + + const FIXED_PART_SIZE: usize = BITMAP_CACHE_REV2_LENGTH; +} + +impl Encode for BitmapCacheRev2 { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.cache_flags.bits()); + write_padding!(dst, 1); + dst.write_u8(self.num_cell_caches); + + for cell_info in self.cache_cell_info.iter() { + cell_info.encode(dst)?; + } + + write_padding!(dst, 12); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for BitmapCacheRev2 { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let cache_flags = CacheFlags::from_bits_truncate(src.read_u16()); + let _padding = src.read_u8(); + let num_cell_caches = src.read_u8(); + + let mut cache_cell_info = [CellInfo::default(); BITMAP_CACHE_REV2_CELL_INFO_NUM]; + + for cell in cache_cell_info.iter_mut() { + *cell = CellInfo::decode(src)?; + } + + read_padding!(src, 12); + + Ok(BitmapCacheRev2 { + cache_flags, + num_cell_caches, + cache_cell_info, + }) + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)] +pub struct CellInfo { + pub num_entries: u32, + pub is_cache_persistent: bool, +} + +impl CellInfo { + const NAME: &'static str = "CellInfo"; + + const FIXED_PART_SIZE: usize = CELL_INFO_LENGTH; +} + +impl Encode for CellInfo { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let mut data = self.num_entries; + + if self.is_cache_persistent { + data |= 1 << 31; + } + + dst.write_u32(data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for CellInfo { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let cell_info = src.read_u32(); + + let num_entries = cell_info & !(1 << 31); + let is_cache_persistent = cell_info >> 31 != 0; + + Ok(CellInfo { + num_entries, + is_cache_persistent, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_cache/tests.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_cache/tests.rs new file mode 100644 index 00000000..abd58ab8 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_cache/tests.rs @@ -0,0 +1,164 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const CACHE_ENTRY_BUFFER: [u8; 4] = [0x64, 0x00, 0x32, 0x00]; + +const BITMAP_CACHE_BUFFER: [u8; 36] = [ + 0x00, 0x00, 0x00, 0x00, // pad + 0x00, 0x00, 0x00, 0x00, // pad + 0x00, 0x00, 0x00, 0x00, // pad + 0x00, 0x00, 0x00, 0x00, // pad + 0x00, 0x00, 0x00, 0x00, // pad + 0x00, 0x00, 0x00, 0x00, // pad + 0xc8, 0x00, // Cache0Entries + 0x00, 0x02, // Cache0MaximumCellSize + 0x58, 0x02, // Cache1Entries + 0x00, 0x08, // Cache1MaximumCellSize + 0xe8, 0x03, // Cache2Entries + 0x00, 0x20, // Cache2MaximumCellSize +]; + +const BITMAP_CACHE_REV2_BUFFER: [u8; 36] = [ + 0x03, 0x00, // CacheFlags + 0x00, // pad2 + 0x03, // NumCellCaches + 0x78, 0x00, 0x00, 0x00, // BitmapCache0CellInfo + 0x78, 0x00, 0x00, 0x00, // BitmapCache1CellInfo + 0xfb, 0x09, 0x00, 0x80, // BitmapCache2CellInfo + 0x00, 0x00, 0x00, 0x00, // BitmapCache3CellInfo + 0x00, 0x00, 0x00, 0x00, // BitmapCache4CellInfo + 0x00, 0x00, 0x00, 0x00, // pad + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +const CELL_INFO_BUFFER: [u8; 4] = [0xfb, 0x09, 0x00, 0x80]; + +static BITMAP_CACHE: LazyLock = LazyLock::new(|| BitmapCache { + caches: [ + CacheEntry { + entries: 200, + max_cell_size: 512, + }, + CacheEntry { + entries: 600, + max_cell_size: 2048, + }, + CacheEntry { + entries: 1000, + max_cell_size: 8192, + }, + ], +}); +static BITMAP_CACHE_REV2: LazyLock = LazyLock::new(|| BitmapCacheRev2 { + cache_flags: CacheFlags::PERSISTENT_KEYS_EXPECTED_FLAG | CacheFlags::ALLOW_CACHE_WAITING_LIST_FLAG, + num_cell_caches: 3, + cache_cell_info: [ + CellInfo { + num_entries: 120, + is_cache_persistent: false, + }, + CellInfo { + num_entries: 120, + is_cache_persistent: false, + }, + CellInfo { + num_entries: 2555, + is_cache_persistent: true, + }, + CellInfo { + num_entries: 0, + is_cache_persistent: false, + }, + CellInfo { + num_entries: 0, + is_cache_persistent: false, + }, + ], +}); +static CELL_INFO: LazyLock = LazyLock::new(|| CellInfo { + num_entries: 2555, + is_cache_persistent: true, +}); +static CACHE_ENTRY: LazyLock = LazyLock::new(|| CacheEntry { + entries: 0x64, + max_cell_size: 0x32, +}); + +#[test] +fn from_buffer_correctly_parses_bitmap_cache_capset() { + let buffer = BITMAP_CACHE_BUFFER.as_ref(); + + assert_eq!(*BITMAP_CACHE, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_bitmap_cache_capset() { + let buffer = encode_vec(&*BITMAP_CACHE).unwrap(); + + assert_eq!(buffer, BITMAP_CACHE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_bitmap_cache_capset() { + assert_eq!(BITMAP_CACHE_BUFFER.len(), BITMAP_CACHE.size()); +} + +#[test] +fn from_buffer_correctly_parses_bitmap_cache_rev2_capset() { + let buffer = BITMAP_CACHE_REV2_BUFFER.as_ref(); + + assert_eq!(*BITMAP_CACHE_REV2, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_bitmap_cache_rev2_capset() { + let buffer = encode_vec(&*BITMAP_CACHE_REV2).unwrap(); + + assert_eq!(buffer, BITMAP_CACHE_REV2_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_bitmap_cache_rev2_capset() { + assert_eq!(BITMAP_CACHE_REV2_BUFFER.len(), BITMAP_CACHE_REV2.size()); +} + +#[test] +fn from_buffer_correctly_parses_cell_info() { + assert_eq!(*CELL_INFO, decode(CELL_INFO_BUFFER.as_ref()).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_cell_info() { + let cell_info = *CELL_INFO; + + let buffer = encode_vec(&cell_info).unwrap(); + + assert_eq!(buffer, CELL_INFO_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_cell_info() { + assert_eq!(CELL_INFO_BUFFER.len(), CELL_INFO.size()); +} + +#[test] +fn from_buffer_correctly_parses_cache_entry() { + assert_eq!(*CACHE_ENTRY, decode(CACHE_ENTRY_BUFFER.as_ref()).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_cache_entry() { + let cache_entry = *CACHE_ENTRY; + + let buffer = encode_vec(&cache_entry).unwrap(); + + assert_eq!(buffer, CACHE_ENTRY_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_cache_entry() { + assert_eq!(CACHE_ENTRY_BUFFER.len(), CACHE_ENTRY.size()); +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs/mod.rs new file mode 100644 index 00000000..f588bc42 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs/mod.rs @@ -0,0 +1,855 @@ +#[cfg(test)] +mod tests; + +use core::fmt::{self, Debug}; +use std::collections::HashMap; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, decode, ensure_fixed_part_size, ensure_size, invalid_field_err, other_err, Decode, DecodeResult, + Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +const RFX_ICAP_VERSION: u16 = 0x0100; +const RFX_ICAP_TILE_SIZE: u16 = 0x40; +const RFX_ICAP_COLOR_CONVERSION: u8 = 1; +const RFX_ICAP_TRANSFORM_BITS: u8 = 1; +const RFX_ICAP_LENGTH: usize = 8; + +const RFX_CAPSET_BLOCK_TYPE: u16 = 0xcbc1; +const RFX_CAPSET_TYPE: u16 = 0xcfc0; +const RFX_CAPSET_STATIC_DATA_LENGTH: usize = 13; + +const RFX_CAPS_BLOCK_TYPE: u16 = 0xcbc0; +const RFX_CAPS_BLOCK_LENGTH: u32 = 8; +const RFX_CAPS_NUM_CAPSETS: u16 = 1; +const RFX_CAPS_STATIC_DATA_LENGTH: usize = 8; + +const RFX_CLIENT_CAPS_CONTAINER_STATIC_DATA_LENGTH: usize = 12; + +const NSCODEC_LENGTH: usize = 3; +const CODEC_STATIC_DATA_LENGTH: usize = 19; + +#[rustfmt::skip] +const GUID_NSCODEC: Guid = Guid(0xca8d_1bb9, 0x000f, 0x154f, 0x58, 0x9f, 0xae, 0x2d, 0x1a, 0x87, 0xe2, 0xd6); +#[rustfmt::skip] +const GUID_REMOTEFX: Guid = Guid(0x7677_2f12, 0xbd72, 0x4463, 0xaf, 0xb3, 0xb7, 0x3c, 0x9c, 0x6f, 0x78, 0x86); +#[rustfmt::skip] +const GUID_IMAGE_REMOTEFX: Guid = Guid(0x2744_ccd4, 0x9d8a, 0x4e74, 0x80, 0x3c, 0x0e, 0xcb, 0xee, 0xa1, 0x9c, 0x54); +#[rustfmt::skip] +const GUID_IGNORE: Guid = Guid(0x9c43_51a6, 0x3535, 0x42ae, 0x91, 0x0c, 0xcd, 0xfc, 0xe5, 0x76, 0x0b, 0x58); +#[rustfmt::skip] +#[cfg(feature="qoi")] +const GUID_QOI: Guid = Guid(0x4dae_9af8, 0xb399, 0x4df6, 0xb4, 0x3a, 0x66, 0x2f, 0xd9, 0xc0, 0xf5, 0xd6); +#[rustfmt::skip] +#[cfg(feature="qoiz")] +const GUID_QOIZ: Guid = Guid(0x229c_c6dc, 0xa860, 0x4b52, 0xb4, 0xd8, 0x05, 0x3a, 0x22, 0xb3, 0x89, 0x2b); + +#[derive(Debug, PartialEq, Eq)] +pub struct Guid(u32, u16, u16, u8, u8, u8, u8, u8, u8, u8, u8); + +impl Guid { + const NAME: &'static str = "Guid"; + + const FIXED_PART_SIZE: usize = 16; +} + +impl Encode for Guid { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.0); + dst.write_u16(self.1); + dst.write_u16(self.2); + dst.write_u8(self.3); + dst.write_u8(self.4); + dst.write_u8(self.5); + dst.write_u8(self.6); + dst.write_u8(self.7); + dst.write_u8(self.8); + dst.write_u8(self.9); + dst.write_u8(self.10); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Guid { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let guid1 = src.read_u32(); + let guid2 = src.read_u16(); + let guid3 = src.read_u16(); + let guid4 = src.read_u8(); + let guid5 = src.read_u8(); + let guid6 = src.read_u8(); + let guid7 = src.read_u8(); + let guid8 = src.read_u8(); + let guid9 = src.read_u8(); + let guid10 = src.read_u8(); + let guid11 = src.read_u8(); + + Ok(Guid( + guid1, guid2, guid3, guid4, guid5, guid6, guid7, guid8, guid9, guid10, guid11, + )) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct BitmapCodecs(pub Vec); + +impl BitmapCodecs { + const NAME: &'static str = "BitmapCodecs"; + + const FIXED_PART_SIZE: usize = 1 /* len */; +} + +impl Encode for BitmapCodecs { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u8(cast_length!("len", self.0.len())?); + + for codec in self.0.iter() { + codec.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.0.iter().map(Encode::size).sum::() + } +} + +impl<'de> Decode<'de> for BitmapCodecs { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let codec_count = src.read_u8(); + + let mut codecs = Vec::with_capacity(usize::from(codec_count)); + for _ in 0..codec_count { + codecs.push(Codec::decode(src)?); + } + + Ok(BitmapCodecs(codecs)) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Codec { + pub id: u8, + pub property: CodecProperty, +} + +impl Codec { + const NAME: &'static str = "Codec"; + + const FIXED_PART_SIZE: usize = CODEC_STATIC_DATA_LENGTH; +} + +impl Encode for Codec { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let guid = match &self.property { + CodecProperty::NsCodec(_) => GUID_NSCODEC, + CodecProperty::RemoteFx(_) => GUID_REMOTEFX, + CodecProperty::ImageRemoteFx(_) => GUID_IMAGE_REMOTEFX, + CodecProperty::Ignore => GUID_IGNORE, + #[cfg(feature = "qoi")] + CodecProperty::Qoi => GUID_QOI, + #[cfg(feature = "qoiz")] + CodecProperty::QoiZ => GUID_QOIZ, + _ => return Err(other_err!("invalid codec")), + }; + guid.encode(dst)?; + + dst.write_u8(self.id); + + match &self.property { + CodecProperty::NsCodec(p) => { + dst.write_u16(cast_length!("len", p.size())?); + p.encode(dst)?; + } + CodecProperty::RemoteFx(p) => { + match p { + RemoteFxContainer::ClientContainer(container) => { + dst.write_u16(cast_length!("len", container.size())?); + container.encode(dst)?; + } + RemoteFxContainer::ServerContainer(size) => { + dst.write_u16(cast_length!("len", *size)?); + let buff = vec![0u8; *size]; + dst.write_slice(&buff); + } + }; + } + CodecProperty::ImageRemoteFx(p) => { + match p { + RemoteFxContainer::ClientContainer(container) => { + dst.write_u16(cast_length!("len", container.size())?); + container.encode(dst)?; + } + RemoteFxContainer::ServerContainer(size) => { + dst.write_u16(cast_length!("len", *size)?); + let buff = vec![0u8; *size]; + dst.write_slice(&buff); + } + }; + } + #[cfg(feature = "qoi")] + CodecProperty::Qoi => dst.write_u16(0), + #[cfg(feature = "qoiz")] + CodecProperty::QoiZ => dst.write_u16(0), + CodecProperty::Ignore => dst.write_u16(0), + CodecProperty::None => dst.write_u16(0), + }; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + match &self.property { + CodecProperty::NsCodec(p) => p.size(), + CodecProperty::RemoteFx(p) => match p { + RemoteFxContainer::ClientContainer(container) => container.size(), + RemoteFxContainer::ServerContainer(size) => *size, + }, + CodecProperty::ImageRemoteFx(p) => match p { + RemoteFxContainer::ClientContainer(container) => container.size(), + RemoteFxContainer::ServerContainer(size) => *size, + }, + #[cfg(feature = "qoi")] + CodecProperty::Qoi => 0, + #[cfg(feature = "qoiz")] + CodecProperty::QoiZ => 0, + CodecProperty::Ignore => 0, + CodecProperty::None => 0, + } + } +} + +impl<'de> Decode<'de> for Codec { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let guid = Guid::decode(src)?; + + let id = src.read_u8(); + let codec_properties_len = usize::from(src.read_u16()); + + ensure_size!(in: src, size: codec_properties_len); + let property_buffer = src.read_slice(codec_properties_len); + + let property = match guid { + GUID_NSCODEC => CodecProperty::NsCodec(decode(property_buffer)?), + GUID_REMOTEFX | GUID_IMAGE_REMOTEFX => { + let byte = property_buffer + .first() + .ok_or_else(|| invalid_field_err!("remotefx property", "must not be empty"))?; + let property = if *byte == 0 { + RemoteFxContainer::ServerContainer(codec_properties_len) + } else { + RemoteFxContainer::ClientContainer(decode(property_buffer)?) + }; + + match guid { + GUID_REMOTEFX => CodecProperty::RemoteFx(property), + GUID_IMAGE_REMOTEFX => CodecProperty::ImageRemoteFx(property), + _ => unreachable!(), + } + } + GUID_IGNORE => CodecProperty::Ignore, + #[cfg(feature = "qoi")] + GUID_QOI => { + if !property_buffer.is_empty() { + return Err(invalid_field_err!("qoi property", "must be empty")); + } + CodecProperty::Qoi + } + #[cfg(feature = "qoiz")] + GUID_QOIZ => { + if !property_buffer.is_empty() { + return Err(invalid_field_err!("qoi property", "must be empty")); + } + CodecProperty::QoiZ + } + _ => CodecProperty::None, + }; + + Ok(Self { id, property }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum RemoteFxContainer { + ClientContainer(RfxClientCapsContainer), + ServerContainer(usize), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum CodecProperty { + NsCodec(NsCodec), + RemoteFx(RemoteFxContainer), + ImageRemoteFx(RemoteFxContainer), + Ignore, + #[cfg(feature = "qoi")] + Qoi, + #[cfg(feature = "qoiz")] + QoiZ, + None, +} + +/// The NsCodec structure advertises properties of the NSCodec Bitmap Codec. +/// +/// # Fields +/// +/// * `is_dynamic_fidelity_allowed` - indicates support for lossy bitmap compression by reducing color fidelity +/// * `is_subsampling_allowed` - indicates support for chroma subsampling +/// * `color_loss_level` - indicates the maximum supported Color Loss Level +/// +/// If received Color Loss Level value is lesser than 1 or greater than 7, it assigns to 1 or 7 respectively. This was made for compatibility with FreeRDP server. +/// +/// # MSDN +/// +/// * [NSCodec Capability Set](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpnsc/0eac0ba8-7bdd-4300-ab8d-9bc784c0a669) +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct NsCodec { + pub is_dynamic_fidelity_allowed: bool, + pub is_subsampling_allowed: bool, + pub color_loss_level: u8, +} + +impl NsCodec { + const NAME: &'static str = "NsCodec"; + + const FIXED_PART_SIZE: usize = NSCODEC_LENGTH; +} + +impl Encode for NsCodec { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u8(u8::from(self.is_dynamic_fidelity_allowed)); + dst.write_u8(u8::from(self.is_subsampling_allowed)); + dst.write_u8(self.color_loss_level); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for NsCodec { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let is_dynamic_fidelity_allowed = src.read_u8() != 0; + let is_subsampling_allowed = src.read_u8() != 0; + + let color_loss_level = src.read_u8().clamp(1, 7); + + Ok(Self { + is_dynamic_fidelity_allowed, + is_subsampling_allowed, + color_loss_level, + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RfxClientCapsContainer { + pub capture_flags: CaptureFlags, + pub caps_data: RfxCaps, +} + +impl RfxClientCapsContainer { + const NAME: &'static str = "RfxClientCapsContainer"; + + const FIXED_PART_SIZE: usize = RFX_CLIENT_CAPS_CONTAINER_STATIC_DATA_LENGTH; +} + +impl Encode for RfxClientCapsContainer { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(cast_length!("len", self.size())?); + dst.write_u32(self.capture_flags.bits()); + dst.write_u32(cast_length!("capsLen", self.caps_data.size())?); + self.caps_data.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.caps_data.size() + } +} + +impl<'de> Decode<'de> for RfxClientCapsContainer { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _length = src.read_u32(); + let capture_flags = CaptureFlags::from_bits_truncate(src.read_u32()); + let _caps_length = src.read_u32(); + let caps_data = RfxCaps::decode(src)?; + + Ok(Self { + capture_flags, + caps_data, + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RfxCaps(pub RfxCapset); + +impl RfxCaps { + const NAME: &'static str = "RfxCaps"; + + const FIXED_PART_SIZE: usize = RFX_CAPS_STATIC_DATA_LENGTH; +} + +impl Encode for RfxCaps { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(RFX_CAPS_BLOCK_TYPE); + dst.write_u32(RFX_CAPS_BLOCK_LENGTH); + dst.write_u16(RFX_CAPS_NUM_CAPSETS); + self.0.encode(dst)?; // capsets data + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.0.size() + } +} + +impl<'de> Decode<'de> for RfxCaps { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let block_type = src.read_u16(); + if block_type != RFX_CAPS_BLOCK_TYPE { + return Err(invalid_field_err!("blockType", "invalid rfx caps block type")); + } + + let block_len = src.read_u32(); + if block_len != RFX_CAPS_BLOCK_LENGTH { + return Err(invalid_field_err!("blockLen", "invalid rfx caps block length")); + } + + let num_capsets = src.read_u16(); + if num_capsets != RFX_CAPS_NUM_CAPSETS { + return Err(invalid_field_err!("numCapsets", "invalid rfx caps num capsets")); + } + + let capsets_data = RfxCapset::decode(src)?; + + Ok(RfxCaps(capsets_data)) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RfxCapset(pub Vec); + +impl RfxCapset { + const NAME: &'static str = "RfxCapset"; + + const FIXED_PART_SIZE: usize = RFX_CAPSET_STATIC_DATA_LENGTH; +} + +impl Encode for RfxCapset { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(RFX_CAPSET_BLOCK_TYPE); + dst.write_u32(cast_length!( + "len", + RFX_CAPSET_STATIC_DATA_LENGTH + self.0.len() * RFX_ICAP_LENGTH + )?); + dst.write_u8(1); // codec id + dst.write_u16(RFX_CAPSET_TYPE); + dst.write_u16(cast_length!("len", self.0.len())?); + dst.write_u16(cast_length!("len", RFX_ICAP_LENGTH)?); + + for rfx in self.0.iter() { + rfx.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.0.len() * RFX_ICAP_LENGTH + } +} + +impl<'de> Decode<'de> for RfxCapset { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let block_type = src.read_u16(); + if block_type != RFX_CAPSET_BLOCK_TYPE { + return Err(invalid_field_err!("blockType", "invalid rfx capset block type")); + } + + let _block_len = src.read_u32(); + + let codec_id = src.read_u8(); + if codec_id != 1 { + return Err(invalid_field_err!("codecId", "invalid rfx codec ID")); + } + + let capset_type = src.read_u16(); + if capset_type != RFX_CAPSET_TYPE { + return Err(invalid_field_err!("capsetType", "invalid rfx capset type")); + } + + let num_icaps = src.read_u16(); + let _icaps_len = src.read_u16(); + + let mut icaps_data = Vec::with_capacity(usize::from(num_icaps)); + for _ in 0..num_icaps { + icaps_data.push(RfxICap::decode(src)?); + } + + Ok(RfxCapset(icaps_data)) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RfxICap { + pub flags: RfxICapFlags, + pub entropy_bits: EntropyBits, +} + +impl RfxICap { + const NAME: &'static str = "RfxICap"; + + const FIXED_PART_SIZE: usize = RFX_ICAP_LENGTH; +} + +impl Encode for RfxICap { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(RFX_ICAP_VERSION); + dst.write_u16(RFX_ICAP_TILE_SIZE); + dst.write_u8(self.flags.bits()); + dst.write_u8(RFX_ICAP_COLOR_CONVERSION); + dst.write_u8(RFX_ICAP_TRANSFORM_BITS); + dst.write_u8(self.entropy_bits.as_u8()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for RfxICap { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version = src.read_u16(); + if version != RFX_ICAP_VERSION { + return Err(invalid_field_err!("version", "invalid rfx icap version")); + } + + let tile_size = src.read_u16(); + if tile_size != RFX_ICAP_TILE_SIZE { + return Err(invalid_field_err!("tileSize", "invalid rfx icap tile size")); + } + + let flags = RfxICapFlags::from_bits_truncate(src.read_u8()); + + let color_conversion = src.read_u8(); + if color_conversion != RFX_ICAP_COLOR_CONVERSION { + return Err(invalid_field_err!("colorConv", "invalid rfx color conversion bits")); + } + + let transform_bits = src.read_u8(); + if transform_bits != RFX_ICAP_TRANSFORM_BITS { + return Err(invalid_field_err!("transformBits", "invalid rfx transform bits")); + } + + let entropy_bits = EntropyBits::from_u8(src.read_u8()) + .ok_or_else(|| invalid_field_err!("entropyBits", "invalid rfx entropy bits"))?; + + Ok(RfxICap { flags, entropy_bits }) + } +} + +#[repr(u8)] +#[derive(PartialEq, Eq, Debug, FromPrimitive, Copy, Clone)] +pub enum EntropyBits { + Rlgr1 = 1, + Rlgr3 = 4, +} + +impl EntropyBits { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CaptureFlags: u32 { + const CARDP_CAPS_CAPTURE_NON_CAC = 1; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct RfxICapFlags: u8 { + const CODEC_MODE = 2; + } +} + +// Those IDs are hard-coded for practical reasons, they are implementation +// details of the IronRDP client. The server should respect the client IDs. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct CodecId(u8); + +pub const CODEC_ID_NONE: CodecId = CodecId(0); +pub const CODEC_ID_REMOTEFX: CodecId = CodecId(3); +pub const CODEC_ID_QOI: CodecId = CodecId(0x0A); +pub const CODEC_ID_QOIZ: CodecId = CodecId(0x0B); + +impl Debug for CodecId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self.0 { + 0 => "None", + 3 => "RemoteFx", + 0x0A => "QOI", + 0x0B => "QOIZ", + _ => "unknown", + }; + write!(f, "CodecId({name})") + } +} + +impl CodecId { + pub const fn from_u8(value: u8) -> Option { + match value { + 0 => Some(CODEC_ID_NONE), + 3 => Some(CODEC_ID_REMOTEFX), + 0x0A => Some(CODEC_ID_QOI), + 0x0B => Some(CODEC_ID_QOIZ), + _ => None, + } + } +} + +fn parse_codecs_config<'a>(codecs: &'a [&'a str]) -> Result, String> { + let mut result = HashMap::new(); + + for &codec_str in codecs { + if let Some((codec_name, state_str)) = codec_str.split_once(':') { + let state = match state_str { + "on" => true, + "off" => false, + _ => return Err(format!("Unhandled configuration: {state_str}")), + }; + + result.insert(codec_name, state); + } else { + // No colon found, assume it's "on" + result.insert(codec_str, true); + } + } + + Ok(result) +} + +/// This function generates a list of client codec capabilities based on the +/// provided configuration. +/// +/// # Arguments +/// +/// * `config` - A slice of string slices that specifies which codecs to include +/// in the capabilities. Codecs can be explicitly turned on ("codec:on") or +/// off ("codec:off"). +/// +/// # List of codecs +/// +/// * `remotefx` (on by default) +/// * `qoi` (on by default, when feature "qoi") +/// * `qoiz` (on by default, when feature "qoiz") +/// +/// # Returns +/// +/// A vector of `Codec` structs representing the codec capabilities, or an error +/// suitable for CLI. +pub fn client_codecs_capabilities(config: &[&str]) -> Result { + if config.contains(&"help") { + return Err(r#" +List of codecs: +- `remotefx` (on by default) +- `qoi` (on by default, when feature "qoi") +- `qoiz` (on by default, when feature "qoiz") +"# + .to_owned()); + } + + let mut config = parse_codecs_config(config)?; + let mut codecs = vec![]; + + if config.remove("remotefx").unwrap_or(true) { + codecs.push(Codec { + id: CODEC_ID_REMOTEFX.0, + property: CodecProperty::RemoteFx(RemoteFxContainer::ClientContainer(RfxClientCapsContainer { + capture_flags: CaptureFlags::empty(), + caps_data: RfxCaps(RfxCapset(vec![RfxICap { + flags: RfxICapFlags::empty(), + entropy_bits: EntropyBits::Rlgr3, + }])), + })), + }); + } + + #[cfg(feature = "qoi")] + if config.remove("qoi").unwrap_or(true) { + codecs.push(Codec { + id: CODEC_ID_QOI.0, + property: CodecProperty::Qoi, + }); + } + + #[cfg(feature = "qoiz")] + if config.remove("qoiz").unwrap_or(true) { + codecs.push(Codec { + id: CODEC_ID_QOIZ.0, + property: CodecProperty::QoiZ, + }); + } + + let codec_names = config.keys().copied().collect::>().join(", "); + if !codec_names.is_empty() { + return Err(format!("Unknown codecs: {codec_names}")); + } + + Ok(BitmapCodecs(codecs)) +} + +/// This function generates a list of server codec capabilities based on the +/// provided configuration. +/// +/// # Arguments +/// +/// * `config` - A slice of string slices that specifies which codecs to include +/// in the capabilities. Codecs can be explicitly turned on ("codec:on") or +/// off ("codec:off"). +/// +/// # List of codecs +/// +/// * `remotefx` (on by default) +/// * `qoi` (on by default, when feature "qoi") +/// * `qoiz` (on by default, when feature "qoiz") +/// +/// # Returns +/// +/// A vector of `Codec` structs representing the codec capabilities, or an help message suitable +/// for CLI errors. +pub fn server_codecs_capabilities(config: &[&str]) -> Result { + if config.contains(&"help") { + return Err(r#" +List of codecs: +- `remotefx` (on by default) +- `qoi` (on by default, when feature "qoi") +- `qoiz` (on by default, when feature "qoiz") +"# + .to_owned()); + } + + let mut config = parse_codecs_config(config)?; + let mut codecs = vec![]; + + if config.remove("remotefx").unwrap_or(true) { + codecs.push(Codec { + id: 0, + property: CodecProperty::RemoteFx(RemoteFxContainer::ServerContainer(1)), + }); + codecs.push(Codec { + id: 0, + property: CodecProperty::ImageRemoteFx(RemoteFxContainer::ServerContainer(1)), + }); + } + + #[cfg(feature = "qoi")] + if config.remove("qoi").unwrap_or(true) { + codecs.push(Codec { + id: 0, + property: CodecProperty::Qoi, + }); + } + + #[cfg(feature = "qoiz")] + if config.remove("qoiz").unwrap_or(true) { + codecs.push(Codec { + id: 0, + property: CodecProperty::QoiZ, + }); + } + + let codec_names = config.keys().copied().collect::>().join(", "); + if !codec_names.is_empty() { + return Err(format!("Unknown codecs: {codec_names}")); + } + + Ok(BitmapCodecs(codecs)) +} diff --git a/ironrdp/src/rdp/capability_sets/bitmap_codecs/test.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs/tests.rs similarity index 69% rename from ironrdp/src/rdp/capability_sets/bitmap_codecs/test.rs rename to crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs/tests.rs index 879965b3..0828f2d6 100644 --- a/ironrdp/src/rdp/capability_sets/bitmap_codecs/test.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs/tests.rs @@ -1,4 +1,6 @@ -use lazy_static::lazy_static; +use std::sync::LazyLock; + +use ironrdp_core::{decode_cursor, encode_vec, DecodeErrorKind}; use super::*; @@ -94,8 +96,7 @@ const NSCODEC_BUFFER: [u8; 3] = [ ]; const CODEC_BUFFER: [u8; 68] = [ - 0x12, 0x2f, 0x77, 0x76, 0x72, 0xbd, 0x63, 0x44, 0xAF, 0xB3, 0xB7, 0x3C, 0x9C, 0x6F, 0x78, - 0x86, // guid + 0x12, 0x2f, 0x77, 0x76, 0x72, 0xbd, 0x63, 0x44, 0xAF, 0xB3, 0xB7, 0x3C, 0x9C, 0x6F, 0x78, 0x86, // guid 0x03, // codec id 0x31, 0x00, // codec properties len 0x31, 0x00, 0x00, 0x00, // length @@ -168,14 +169,27 @@ const BITMAP_CODECS_BUFFER: [u8; 91] = [ 0x03, // color loss level ]; -lazy_static! { - #[rustfmt::skip] - pub static ref GUID: Guid = Guid(0xca8d_1bb9, 0x000f, 0x154f, 0x58, 0x9f, 0xae, 0x2d, 0x1a, 0x87, 0xe2, 0xd6); - pub static ref RFX_ICAP: RfxICap = RfxICap { - flags: RfxICapFlags::CODEC_MODE, - entropy_bits: EntropyBits::Rlgr3, - }; - pub static ref RFX_CAPSET: RfxCapset = RfxCapset(vec![ +static GUID: LazyLock = LazyLock::new(|| { + Guid( + 0xca8d_1bb9, + 0x000f, + 0x154f, + 0x58, + 0x9f, + 0xae, + 0x2d, + 0x1a, + 0x87, + 0xe2, + 0xd6, + ) +}); +static RFX_ICAP: LazyLock = LazyLock::new(|| RfxICap { + flags: RfxICapFlags::CODEC_MODE, + entropy_bits: EntropyBits::Rlgr3, +}); +static RFX_CAPSET: LazyLock = LazyLock::new(|| { + RfxCapset(vec![ RfxICap { flags: RfxICapFlags::empty(), entropy_bits: EntropyBits::Rlgr1, @@ -183,9 +197,11 @@ lazy_static! { RfxICap { flags: RfxICapFlags::CODEC_MODE, entropy_bits: EntropyBits::Rlgr3, - } - ]); - pub static ref RFX_CAPS: RfxCaps = RfxCaps(RfxCapset(vec![ + }, + ]) +}); +static RFX_CAPS: LazyLock = LazyLock::new(|| { + RfxCaps(RfxCapset(vec![ RfxICap { flags: RfxICapFlags::empty(), entropy_bits: EntropyBits::Rlgr1, @@ -193,9 +209,30 @@ lazy_static! { RfxICap { flags: RfxICapFlags::CODEC_MODE, entropy_bits: EntropyBits::Rlgr3, - } - ])); - pub static ref RFX_CLIENT_CAPS_CONTAINER: RfxClientCapsContainer = RfxClientCapsContainer { + }, + ])) +}); +static RFX_CLIENT_CAPS_CONTAINER: LazyLock = LazyLock::new(|| RfxClientCapsContainer { + capture_flags: CaptureFlags::CARDP_CAPS_CAPTURE_NON_CAC, + caps_data: RfxCaps(RfxCapset(vec![ + RfxICap { + flags: RfxICapFlags::empty(), + entropy_bits: EntropyBits::Rlgr1, + }, + RfxICap { + flags: RfxICapFlags::CODEC_MODE, + entropy_bits: EntropyBits::Rlgr3, + }, + ])), +}); +static NSCODEC: LazyLock = LazyLock::new(|| NsCodec { + is_dynamic_fidelity_allowed: true, + is_subsampling_allowed: true, + color_loss_level: 3, +}); +static CODEC: LazyLock = LazyLock::new(|| Codec { + id: 3, + property: CodecProperty::RemoteFx(RemoteFxContainer::ClientContainer(RfxClientCapsContainer { capture_flags: CaptureFlags::CARDP_CAPS_CAPTURE_NON_CAC, caps_data: RfxCaps(RfxCapset(vec![ RfxICap { @@ -205,18 +242,19 @@ lazy_static! { RfxICap { flags: RfxICapFlags::CODEC_MODE, entropy_bits: EntropyBits::Rlgr3, - } + }, ])), - }; - pub static ref NSCODEC: NsCodec = NsCodec { - is_dynamic_fidelity_allowed: true, - is_subsampling_allowed: true, - color_loss_level: 3, - }; - pub static ref CODEC: Codec = Codec { - id: 3, - property: CodecProperty::RemoteFx(RemoteFxContainer::ClientContainer( - RfxClientCapsContainer { + })), +}); +static CODEC_SERVER_MODE: LazyLock = LazyLock::new(|| Codec { + id: 0, + property: CodecProperty::ImageRemoteFx(RemoteFxContainer::ServerContainer(4)), +}); +static BITMAP_CODECS: LazyLock = LazyLock::new(|| { + BitmapCodecs(vec![ + Codec { + id: 3, + property: CodecProperty::RemoteFx(RemoteFxContainer::ClientContainer(RfxClientCapsContainer { capture_flags: CaptureFlags::CARDP_CAPS_CAPTURE_NON_CAC, caps_data: RfxCaps(RfxCapset(vec![ RfxICap { @@ -226,33 +264,9 @@ lazy_static! { RfxICap { flags: RfxICapFlags::CODEC_MODE, entropy_bits: EntropyBits::Rlgr3, - } + }, ])), - } - )), - }; - pub static ref CODEC_SERVER_MODE: Codec = Codec { - id: 0, - property: CodecProperty::ImageRemoteFx(RemoteFxContainer::ServerContainer(4)), - }; - pub static ref BITMAP_CODECS: BitmapCodecs = BitmapCodecs(vec![ - Codec { - id: 3, - property: CodecProperty::RemoteFx(RemoteFxContainer::ClientContainer( - RfxClientCapsContainer { - capture_flags: CaptureFlags::CARDP_CAPS_CAPTURE_NON_CAC, - caps_data: RfxCaps(RfxCapset(vec![ - RfxICap { - flags: RfxICapFlags::empty(), - entropy_bits: EntropyBits::Rlgr1, - }, - RfxICap { - flags: RfxICapFlags::CODEC_MODE, - entropy_bits: EntropyBits::Rlgr3, - } - ])), - } - )) + })), }, Codec { id: 1, @@ -260,209 +274,163 @@ lazy_static! { is_dynamic_fidelity_allowed: true, is_subsampling_allowed: true, color_loss_level: 3, - }) + }), }, - ]); -} + ]) +}); #[test] fn from_buffer_correctly_parses_guid() { - assert_eq!(*GUID, Guid::from_buffer(GUID_BUFFER.as_ref()).unwrap()); + assert_eq!(*GUID, decode(GUID_BUFFER.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_guid() { - let mut buffer = Vec::new(); - GUID.to_buffer(&mut buffer).unwrap(); + let buffer = encode_vec(&*GUID).unwrap(); assert_eq!(buffer, GUID_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_guid() { - assert_eq!(GUID_BUFFER.len(), GUID.buffer_length()); + assert_eq!(GUID_BUFFER.len(), GUID.size()); } #[test] fn from_buffer_correctly_parses_rfx_icap() { - assert_eq!( - *RFX_ICAP, - RfxICap::from_buffer(RFX_ICAP_BUFFER.as_ref()).unwrap() - ); + assert_eq!(*RFX_ICAP, decode(RFX_ICAP_BUFFER.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_rfx_icap() { - let mut buffer = Vec::new(); - - RFX_ICAP.to_buffer(&mut buffer).unwrap(); - + let buffer = encode_vec(&*RFX_ICAP).unwrap(); assert_eq!(buffer, RFX_ICAP_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_rfx_icap() { - assert_eq!(RFX_ICAP_BUFFER.len(), RFX_ICAP.buffer_length()); + assert_eq!(RFX_ICAP_BUFFER.len(), RFX_ICAP.size()); } #[test] fn from_buffer_correctly_parses_rfx_capset() { - assert_eq!( - *RFX_CAPSET, - RfxCapset::from_buffer(RFX_CAPSET_BUFFER.as_ref()).unwrap() - ); + assert_eq!(*RFX_CAPSET, decode(RFX_CAPSET_BUFFER.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_rfx_capset() { - let mut buffer = Vec::new(); - - RFX_CAPSET.to_buffer(&mut buffer).unwrap(); + let buffer = encode_vec(&*RFX_CAPSET).unwrap(); assert_eq!(buffer, RFX_CAPSET_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_rfx_capset() { - assert_eq!(RFX_CAPSET_BUFFER.len(), RFX_CAPSET.buffer_length()); + assert_eq!(RFX_CAPSET_BUFFER.len(), RFX_CAPSET.size()); } #[test] fn from_buffer_correctly_parses_rfx_caps() { - assert_eq!( - *RFX_CAPS, - RfxCaps::from_buffer(RFX_CAPS_BUFFER.as_ref()).unwrap() - ); + assert_eq!(*RFX_CAPS, decode(RFX_CAPS_BUFFER.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_rfx_caps() { - let mut buffer = Vec::new(); - - RFX_CAPS.to_buffer(&mut buffer).unwrap(); - + let buffer = encode_vec(&*RFX_CAPS).unwrap(); assert_eq!(buffer, RFX_CAPS_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_rfx_caps() { - assert_eq!(RFX_CAPS_BUFFER.len(), RFX_CAPS.buffer_length()); + assert_eq!(RFX_CAPS_BUFFER.len(), RFX_CAPS.size()); } #[test] fn from_buffer_correctly_parses_rfx_client_caps_container() { assert_eq!( *RFX_CLIENT_CAPS_CONTAINER, - RfxClientCapsContainer::from_buffer(RFX_CLIENT_CAPS_CONTAINER_BUFFER.as_ref()).unwrap() + decode(RFX_CLIENT_CAPS_CONTAINER_BUFFER.as_ref()).unwrap() ); } #[test] fn to_buffer_correctly_serializes_rfx_client_caps_container() { - let mut buffer = Vec::new(); - - RFX_CLIENT_CAPS_CONTAINER.to_buffer(&mut buffer).unwrap(); - + let buffer = encode_vec(&*RFX_CLIENT_CAPS_CONTAINER).unwrap(); assert_eq!(buffer, RFX_CLIENT_CAPS_CONTAINER_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_rfx_client_caps_container() { - assert_eq!( - RFX_CLIENT_CAPS_CONTAINER_BUFFER.len(), - RFX_CLIENT_CAPS_CONTAINER.buffer_length() - ); + assert_eq!(RFX_CLIENT_CAPS_CONTAINER_BUFFER.len(), RFX_CLIENT_CAPS_CONTAINER.size()); } #[test] fn from_buffer_correctly_parses_nscodec() { - assert_eq!( - *NSCODEC, - NsCodec::from_buffer(NSCODEC_BUFFER.as_ref()).unwrap() - ); + assert_eq!(*NSCODEC, decode(NSCODEC_BUFFER.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_nscodec() { - let mut buffer = Vec::new(); - - NSCODEC.to_buffer(&mut buffer).unwrap(); - + let buffer = encode_vec(&*NSCODEC).unwrap(); assert_eq!(buffer, NSCODEC_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_nscodec() { - assert_eq!(NSCODEC_BUFFER.len(), NSCODEC.buffer_length()); + assert_eq!(NSCODEC_BUFFER.len(), NSCODEC.size()); } #[test] fn from_buffer_correctly_parses_codec() { - assert_eq!(*CODEC, Codec::from_buffer(CODEC_BUFFER.as_ref()).unwrap()); + assert_eq!(*CODEC, decode(CODEC_BUFFER.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_codec() { - let mut buffer = Vec::new(); - - CODEC.to_buffer(&mut buffer).unwrap(); - + let buffer = encode_vec(&*CODEC).unwrap(); assert_eq!(buffer, CODEC_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_codec() { - assert_eq!(CODEC_BUFFER.len(), CODEC.buffer_length()); + assert_eq!(CODEC_BUFFER.len(), CODEC.size()); } #[test] fn from_buffer_correctly_parses_codec_server_mode() { - assert_eq!( - *CODEC_SERVER_MODE, - Codec::from_buffer(CODEC_SERVER_MODE_BUFFER.as_ref()).unwrap() - ); + assert_eq!(*CODEC_SERVER_MODE, decode(CODEC_SERVER_MODE_BUFFER.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_codec_server_mode() { - let mut buffer = Vec::new(); - - CODEC_SERVER_MODE.to_buffer(&mut buffer).unwrap(); - + let buffer = encode_vec(&*CODEC_SERVER_MODE).unwrap(); assert_eq!(buffer, CODEC_SERVER_MODE_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_codec_server_mode() { - assert_eq!(CODEC_BUFFER.len(), CODEC.buffer_length()); + assert_eq!(CODEC_BUFFER.len(), CODEC.size()); } #[test] fn from_buffer_correctly_parses_bitmap_codecs() { - assert_eq!( - *BITMAP_CODECS, - BitmapCodecs::from_buffer(BITMAP_CODECS_BUFFER.as_ref()).unwrap() - ); + assert_eq!(*BITMAP_CODECS, decode(BITMAP_CODECS_BUFFER.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_bitmap_codes() { - let mut buffer = Vec::new(); - - BITMAP_CODECS.to_buffer(&mut buffer).unwrap(); - + let buffer = encode_vec(&*BITMAP_CODECS).unwrap(); assert_eq!(buffer, BITMAP_CODECS_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_bitmap_codec() { - assert_eq!(BITMAP_CODECS_BUFFER.len(), BITMAP_CODECS.buffer_length()); + assert_eq!(BITMAP_CODECS_BUFFER.len(), BITMAP_CODECS.size()); } #[test] fn codec_with_invalid_property_length_handles_correctly() { let codec_buffer: [u8; 68] = [ - 0x12, 0x2f, 0x77, 0x76, 0x72, 0xbd, 0x63, 0x44, 0xAF, 0xB3, 0xB7, 0x3C, 0x9C, 0x6F, 0x78, - 0x86, // guid + 0x12, 0x2f, 0x77, 0x76, 0x72, 0xbd, 0x63, 0x44, 0xAF, 0xB3, 0xB7, 0x3C, 0x9C, 0x6F, 0x78, 0x86, // guid 0x03, // codec id 0x00, 0x00, // codec properties len 0x31, 0x00, 0x00, 0x00, // length @@ -491,9 +459,9 @@ fn codec_with_invalid_property_length_handles_correctly() { 0x04, // entropy_bits ]; - match Codec::from_buffer(codec_buffer.as_ref()) { - Err(CapabilitySetsError::InvalidPropertyLength) => (), - Err(_e) => panic!("wrong error type"), + match decode::(codec_buffer.as_ref()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::InvalidField { .. }) => (), + Err(e) => panic!("wrong error type: {e}"), _ => panic!("error expected"), } } @@ -501,8 +469,8 @@ fn codec_with_invalid_property_length_handles_correctly() { #[test] fn codec_with_empty_property_length_and_ignore_guid_handles_correctly() { let codec_buffer: [u8; 19] = [ - 0xa6, 0x51, 0x43, 0x9c, 0x35, 0x35, 0xae, 0x42, 0x91, 0x0c, 0xcd, 0xfc, 0xe5, 0x76, 0x0b, - 0x58, 0x00, // codec id + 0xa6, 0x51, 0x43, 0x9c, 0x35, 0x35, 0xae, 0x42, 0x91, 0x0c, 0xcd, 0xfc, 0xe5, 0x76, 0x0b, 0x58, + 0x00, // codec id 0x00, 0x00, // codec properties len ]; @@ -511,14 +479,14 @@ fn codec_with_empty_property_length_and_ignore_guid_handles_correctly() { property: CodecProperty::Ignore, }; - assert_eq!(codec, Codec::from_buffer(codec_buffer.as_ref()).unwrap()); + assert_eq!(codec, decode(codec_buffer.as_ref()).unwrap()); } #[test] fn codec_with_property_length_and_ignore_guid_handled_correctly() { let codec_buffer = vec![ - 0xa6u8, 0x51, 0x43, 0x9c, 0x35, 0x35, 0xae, 0x42, 0x91, 0x0c, 0xcd, 0xfc, 0xe5, 0x76, 0x0b, - 0x58, 0x00, // codec id + 0xa6u8, 0x51, 0x43, 0x9c, 0x35, 0x35, 0xae, 0x42, 0x91, 0x0c, 0xcd, 0xfc, 0xe5, 0x76, 0x0b, 0x58, + 0x00, // codec id 0x0f, 0x00, // codec properties len 0xa6, 0x51, 0x43, 0x9c, 0x35, 0x35, 0xae, 0x42, 0x91, 0x0c, 0xcd, 0xfc, 0xe5, 0x76, 0x0b, ]; @@ -528,17 +496,16 @@ fn codec_with_property_length_and_ignore_guid_handled_correctly() { property: CodecProperty::Ignore, }; - let mut slice = codec_buffer.as_slice(); - - assert_eq!(codec, Codec::from_buffer(&mut slice).unwrap()); - assert!(slice.is_empty()); + let slice = codec_buffer.as_slice(); + let mut cur = ReadCursor::new(slice); + assert_eq!(codec, decode_cursor(&mut cur).unwrap()); + assert!(cur.is_empty()); } #[test] fn ns_codec_with_too_high_color_loss_level_handled_correctly() { let codec_buffer = vec![ - 0xb9, 0x1b, 0x8d, 0xca, 0x0f, 0x00, 0x4f, 0x15, 0x58, 0x9F, 0xAE, 0x2D, 0x1A, 0x87, 0xE2, - 0xd6, // guid + 0xb9, 0x1b, 0x8d, 0xca, 0x0f, 0x00, 0x4f, 0x15, 0x58, 0x9F, 0xAE, 0x2D, 0x1A, 0x87, 0xE2, 0xd6, // guid 0x00, // codec id 0x03, 0x00, // codec properties len 0x01, // allow dynamic fidelity @@ -555,17 +522,13 @@ fn ns_codec_with_too_high_color_loss_level_handled_correctly() { }), }; - assert_eq!( - codec, - Codec::from_buffer(&mut codec_buffer.as_slice()).unwrap() - ); + assert_eq!(codec, decode(codec_buffer.as_slice()).unwrap()); } #[test] fn ns_codec_with_too_low_color_loss_level_handled_correctly() { let codec_buffer = vec![ - 0xb9, 0x1b, 0x8d, 0xca, 0x0f, 0x00, 0x4f, 0x15, 0x58, 0x9F, 0xAE, 0x2D, 0x1A, 0x87, 0xE2, - 0xd6, // guid + 0xb9, 0x1b, 0x8d, 0xca, 0x0f, 0x00, 0x4f, 0x15, 0x58, 0x9F, 0xAE, 0x2D, 0x1A, 0x87, 0xE2, 0xd6, // guid 0x00, // codec id 0x03, 0x00, // codec properties len 0x01, // allow dynamic fidelity @@ -582,8 +545,5 @@ fn ns_codec_with_too_low_color_loss_level_handled_correctly() { }), }; - assert_eq!( - codec, - Codec::from_buffer(&mut codec_buffer.as_slice()).unwrap() - ); + assert_eq!(codec, decode(codec_buffer.as_slice()).unwrap()); } diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/brush/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/brush/mod.rs new file mode 100644 index 00000000..d8c3e6d5 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/brush/mod.rs @@ -0,0 +1,68 @@ +#[cfg(test)] +mod tests; + +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +const BRUSH_LENGTH: usize = 4; + +#[repr(u32)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] +pub enum SupportLevel { + Default = 0, + Color8x8 = 1, + ColorFull = 2, +} + +impl SupportLevel { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Brush { + pub support_level: SupportLevel, +} + +impl Brush { + const NAME: &'static str = "Brush"; + + const FIXED_PART_SIZE: usize = BRUSH_LENGTH; +} + +impl Encode for Brush { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.support_level.as_u32()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Brush { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let support_level = SupportLevel::from_u32(src.read_u32()) + .ok_or_else(|| invalid_field_err!("supportLevel", "invalid brush support level"))?; + + Ok(Brush { support_level }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/brush/tests.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/brush/tests.rs new file mode 100644 index 00000000..75d9612a --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/brush/tests.rs @@ -0,0 +1,30 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const BRUSH_BUFFER: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; + +static BRUSH: LazyLock = LazyLock::new(|| Brush { + support_level: SupportLevel::Color8x8, +}); + +#[test] +fn from_buffer_successfully_parses_brush_capset() { + assert_eq!(*BRUSH, decode(BRUSH_BUFFER.as_ref()).unwrap()); +} + +#[test] +fn to_buffer_successfully_serializes_brush_capset() { + let brush = BRUSH.clone(); + + let buffer = encode_vec(&brush).unwrap(); + + assert_eq!(buffer, BRUSH_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_input_capset() { + assert_eq!(BRUSH_BUFFER.len(), BRUSH.size()); +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/frame_acknowledge.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/frame_acknowledge.rs new file mode 100644 index 00000000..ea090c5e --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/frame_acknowledge.rs @@ -0,0 +1,75 @@ +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FrameAcknowledge { + pub max_unacknowledged_frame_count: u32, +} + +impl FrameAcknowledge { + const NAME: &'static str = "FrameAcknowledge"; + + const FIXED_PART_SIZE: usize = 4 /* maxUnackFrameCount */; +} + +impl Encode for FrameAcknowledge { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.max_unacknowledged_frame_count); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for FrameAcknowledge { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let max_unacknowledged_frame_count = src.read_u32(); + + Ok(Self { + max_unacknowledged_frame_count, + }) + } +} + +#[cfg(test)] +mod test { + use ironrdp_core::{decode, encode_vec}; + + use super::*; + + const FRAME_ACKNOWLEDGE_PDU_BUFFER: [u8; 4] = [0xf4, 0xf3, 0xf2, 0xf1]; + const FRAME_ACKNOWLEDGE_PDU: FrameAcknowledge = FrameAcknowledge { + max_unacknowledged_frame_count: 0xf1f2_f3f4, + }; + + #[test] + fn from_buffer_correctly_parses_frame_acknowledge() { + assert_eq!( + FRAME_ACKNOWLEDGE_PDU, + decode(FRAME_ACKNOWLEDGE_PDU_BUFFER.as_ref()).unwrap() + ); + } + + #[test] + fn to_buffer_correctly_serializes_frame_acknowledge() { + let expected = FRAME_ACKNOWLEDGE_PDU_BUFFER.as_ref(); + + let buffer = encode_vec(&FRAME_ACKNOWLEDGE_PDU).unwrap(); + assert_eq!(expected, buffer.as_slice()); + } + + #[test] + fn buffer_length_is_correct_for_frame_acknowledge() { + assert_eq!(FRAME_ACKNOWLEDGE_PDU_BUFFER.len(), FRAME_ACKNOWLEDGE_PDU.size()); + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/general/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/general/mod.rs new file mode 100644 index 00000000..074a9290 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/general/mod.rs @@ -0,0 +1,197 @@ +#[cfg(test)] +mod tests; + +use std::fmt; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; + +const GENERAL_LENGTH: usize = 20; +pub const PROTOCOL_VER: u16 = 0x0200; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MajorPlatformType(u16); + +impl fmt::Debug for MajorPlatformType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match *self { + Self::UNSPECIFIED => "UNSPECIFIED", + Self::WINDOWS => "WINDOWS", + Self::OS2 => "OS2", + Self::MACINTOSH => "MACINTOSH", + Self::UNIX => "UNIX", + Self::IOS => "IOS", + Self::OSX => "OSX", + Self::ANDROID => "ANDROID", + Self::CHROMEOS => "CHROMEOS", + _ => "UNKNOWN", + }; + + write!(f, "MajorPlatformType(0x{:02X}-{name})", self.0) + } +} + +impl MajorPlatformType { + pub const UNSPECIFIED: Self = Self(0); + pub const WINDOWS: Self = Self(1); + pub const OS2: Self = Self(2); + pub const MACINTOSH: Self = Self(3); + pub const UNIX: Self = Self(4); + pub const IOS: Self = Self(5); + pub const OSX: Self = Self(6); + pub const ANDROID: Self = Self(7); + pub const CHROMEOS: Self = Self(8); +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MinorPlatformType(u16); + +impl fmt::Debug for MinorPlatformType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match *self { + Self::UNSPECIFIED => "UNSPECIFIED", + Self::WINDOWS_31X => "WINDOWS_31X", + Self::WINDOWS_95 => "WINDOWS_95", + Self::WINDOWS_NT => "WINDOWS_NT", + Self::OS2V21 => "OS2_V21", + Self::POWER_PC => "POWER_PC", + Self::MACINTOSH => "MACINTOSH", + Self::NATIVE_XSERVER => "NATIVE_XSERVER", + Self::PSEUDO_XSERVER => "PSEUDO_XSERVER", + Self::WINDOWS_RT => "WINDOWS_RT", + _ => "UNKNOWN", + }; + + write!(f, "MinorPlatformType(0x{:02X}-{name})", self.0) + } +} + +impl MinorPlatformType { + pub const UNSPECIFIED: Self = Self(0); + pub const WINDOWS_31X: Self = Self(1); + pub const WINDOWS_95: Self = Self(2); + pub const WINDOWS_NT: Self = Self(3); + pub const OS2V21: Self = Self(4); + pub const POWER_PC: Self = Self(5); + pub const MACINTOSH: Self = Self(6); + pub const NATIVE_XSERVER: Self = Self(7); + pub const PSEUDO_XSERVER: Self = Self(8); + pub const WINDOWS_RT: Self = Self(9); +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct GeneralExtraFlags: u16 { + const FASTPATH_OUTPUT_SUPPORTED = 0x0001; + const NO_BITMAP_COMPRESSION_HDR = 0x0400; + const LONG_CREDENTIALS_SUPPORTED = 0x0004; + const AUTORECONNECT_SUPPORTED = 0x0008; + const ENC_SALTED_CHECKSUM = 0x0010; + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct General { + pub major_platform_type: MajorPlatformType, + pub minor_platform_type: MinorPlatformType, + pub protocol_version: u16, + pub extra_flags: GeneralExtraFlags, + pub refresh_rect_support: bool, + pub suppress_output_support: bool, +} + +impl General { + const NAME: &'static str = "General"; + + const FIXED_PART_SIZE: usize = GENERAL_LENGTH; +} + +impl Default for General { + fn default() -> Self { + Self { + major_platform_type: MajorPlatformType::UNSPECIFIED, + minor_platform_type: MinorPlatformType::UNSPECIFIED, + protocol_version: PROTOCOL_VER, + extra_flags: GeneralExtraFlags::empty(), + refresh_rect_support: Default::default(), + suppress_output_support: Default::default(), + } + } +} + +impl Encode for General { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.major_platform_type.0); + dst.write_u16(self.minor_platform_type.0); + dst.write_u16(PROTOCOL_VER); + dst.write_u16(0); // padding + dst.write_u16(0); // generalCompressionTypes + dst.write_u16(self.extra_flags.bits()); + dst.write_u16(0); // updateCapabilityFlag + dst.write_u16(0); // remoteUnshareFlag + dst.write_u16(0); // generalCompressionLevel + dst.write_u8(u8::from(self.refresh_rect_support)); + dst.write_u8(u8::from(self.suppress_output_support)); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for General { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let major_platform_type = MajorPlatformType(src.read_u16()); + let minor_platform_type = MinorPlatformType(src.read_u16()); + + let protocol_version = src.read_u16(); + + let _padding = src.read_u16(); + + let compression_types = src.read_u16(); + if compression_types != 0 { + return Err(invalid_field_err!("compressionTypes", "invalid compression types")); + } + + let extra_flags = GeneralExtraFlags::from_bits_truncate(src.read_u16()); + + let update_cap_flags = src.read_u16(); + if update_cap_flags != 0 { + return Err(invalid_field_err!("updateCapFlags", "invalid update cap flags")); + } + + let remote_unshare_flag = src.read_u16(); + if remote_unshare_flag != 0 { + return Err(invalid_field_err!("remoteUnshareFlags", "invalid remote unshare flag")); + } + + let compression_level = src.read_u16(); + if compression_level != 0 { + return Err(invalid_field_err!("compressionLevel", "invalid compression level")); + } + + let refresh_rect_support = src.read_u8() != 0; + let suppress_output_support = src.read_u8() != 0; + + Ok(General { + major_platform_type, + minor_platform_type, + protocol_version, + extra_flags, + refresh_rect_support, + suppress_output_support, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/general/tests.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/general/tests.rs new file mode 100644 index 00000000..55b09cbf --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/general/tests.rs @@ -0,0 +1,55 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const GENERAL_CAPSET_BUFFER: [u8; 20] = [ + 0x01, 0x00, // osMajorType + 0x03, 0x00, // osMinorType + 0x00, 0x02, // protocolVersion + 0x00, 0x00, // pad2octetsA + 0x00, 0x00, // generalCompressionTypes + 0x1d, 0x04, // extraFlags + 0x00, 0x00, // updateCapabilityFlag + 0x00, 0x00, // remoteUnshareFlag + 0x00, 0x00, // generalCompressionLevel + 0x00, // refreshRectSupport + 0x00, // suppressOutputSupport +]; + +static CAPSET_GENERAL: LazyLock = LazyLock::new(|| General { + major_platform_type: MajorPlatformType::WINDOWS, + minor_platform_type: MinorPlatformType::WINDOWS_NT, + protocol_version: PROTOCOL_VER, + extra_flags: GeneralExtraFlags::FASTPATH_OUTPUT_SUPPORTED + | GeneralExtraFlags::LONG_CREDENTIALS_SUPPORTED + | GeneralExtraFlags::AUTORECONNECT_SUPPORTED + | GeneralExtraFlags::ENC_SALTED_CHECKSUM + | GeneralExtraFlags::NO_BITMAP_COMPRESSION_HDR, + refresh_rect_support: false, + suppress_output_support: false, +}); + +#[test] +fn from_buffer_correctly_parses_general_capset() { + let buffer = GENERAL_CAPSET_BUFFER.as_ref(); + + assert_eq!(*CAPSET_GENERAL, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_general_capset() { + let capset = CAPSET_GENERAL.clone(); + + let buffer = encode_vec(&capset).unwrap(); + + assert_eq!(buffer, GENERAL_CAPSET_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_general_capset() { + let correct_buffer_length = GENERAL_CAPSET_BUFFER.len(); + + assert_eq!(correct_buffer_length, CAPSET_GENERAL.size()); +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/glyph_cache/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/glyph_cache/mod.rs new file mode 100644 index 00000000..235c2248 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/glyph_cache/mod.rs @@ -0,0 +1,136 @@ +#[cfg(test)] +mod tests; + +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +pub const GLYPH_CACHE_NUM: usize = 10; + +const GLYPH_CACHE_LENGTH: usize = 48; +const CACHE_DEFINITION_LENGTH: usize = 4; + +#[repr(u16)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] +pub enum GlyphSupportLevel { + None = 0, + Partial = 1, + Full = 2, + Encode = 3, +} + +impl GlyphSupportLevel { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)] +pub struct CacheDefinition { + pub entries: u16, + pub max_cell_size: u16, +} + +impl CacheDefinition { + const NAME: &'static str = "CacheDefinition"; + + const FIXED_PART_SIZE: usize = CACHE_DEFINITION_LENGTH; +} + +impl Encode for CacheDefinition { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.entries); + dst.write_u16(self.max_cell_size); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for CacheDefinition { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let entries = src.read_u16(); + let max_cell_size = src.read_u16(); + + Ok(CacheDefinition { entries, max_cell_size }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct GlyphCache { + pub glyph_cache: [CacheDefinition; GLYPH_CACHE_NUM], + pub frag_cache: CacheDefinition, + pub glyph_support_level: GlyphSupportLevel, +} + +impl GlyphCache { + const NAME: &'static str = "GlyphCache"; + + const FIXED_PART_SIZE: usize = GLYPH_CACHE_LENGTH; +} + +impl Encode for GlyphCache { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + for glyph in self.glyph_cache.iter() { + glyph.encode(dst)?; + } + + self.frag_cache.encode(dst)?; + + dst.write_u16(self.glyph_support_level.as_u16()); + write_padding!(dst, 2); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for GlyphCache { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let mut glyph_cache = [CacheDefinition::default(); GLYPH_CACHE_NUM]; + + for glyph in glyph_cache.iter_mut() { + *glyph = CacheDefinition::decode(src)?; + } + + let frag_cache = CacheDefinition::decode(src)?; + let glyph_support_level = GlyphSupportLevel::from_u16(src.read_u16()) + .ok_or_else(|| invalid_field_err!("glyphSupport", "invalid glyph support level"))?; + let _padding = src.read_u16(); + + Ok(GlyphCache { + glyph_cache, + frag_cache, + glyph_support_level, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/glyph_cache/tests.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/glyph_cache/tests.rs new file mode 100644 index 00000000..fff93d06 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/glyph_cache/tests.rs @@ -0,0 +1,108 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const GLYPH_CACHE_BUFFER: [u8; 48] = [ + 0xfe, 0x00, 0x04, 0x00, 0xfe, 0x00, 0x04, 0x00, 0xfe, 0x00, 0x08, 0x00, 0xfe, 0x00, 0x08, 0x00, 0xfe, 0x00, 0x10, + 0x00, 0xfe, 0x00, 0x20, 0x00, 0xfe, 0x00, 0x40, 0x00, 0xfe, 0x00, 0x80, 0x00, 0xfe, 0x00, 0x00, 0x01, 0x40, 0x00, + 0x00, 0x08, // GlyphCache + 0x00, 0x01, 0x00, 0x01, // FragCache + 0x03, 0x00, // GlyphSupportLevel + 0x00, 0x00, // pad2octets +]; + +const CACHE_DEFINITION_BUFFER: [u8; 4] = [0xfe, 0x00, 0x04, 0x00]; + +static GLYPH_CACHE: LazyLock = LazyLock::new(|| GlyphCache { + glyph_cache: [ + CacheDefinition { + entries: 254, + max_cell_size: 4, + }, + CacheDefinition { + entries: 254, + max_cell_size: 4, + }, + CacheDefinition { + entries: 254, + max_cell_size: 8, + }, + CacheDefinition { + entries: 254, + max_cell_size: 8, + }, + CacheDefinition { + entries: 254, + max_cell_size: 16, + }, + CacheDefinition { + entries: 254, + max_cell_size: 32, + }, + CacheDefinition { + entries: 254, + max_cell_size: 64, + }, + CacheDefinition { + entries: 254, + max_cell_size: 128, + }, + CacheDefinition { + entries: 254, + max_cell_size: 256, + }, + CacheDefinition { + entries: 64, + max_cell_size: 2048, + }, + ], + frag_cache: CacheDefinition { + entries: 256, + max_cell_size: 256, + }, + glyph_support_level: GlyphSupportLevel::Encode, +}); +static CACHE_DEFINITION: LazyLock = LazyLock::new(|| CacheDefinition { + entries: 254, + max_cell_size: 4, +}); + +#[test] +fn from_buffer_correctly_parses_glyph_cache_capset() { + assert_eq!(*GLYPH_CACHE, decode(GLYPH_CACHE_BUFFER.as_ref()).unwrap(),); +} + +#[test] +fn to_buffer_correctly_serializes_glyph_cache_capset() { + let glyph_cache = GLYPH_CACHE.clone(); + + let buffer = encode_vec(&glyph_cache).unwrap(); + + assert_eq!(buffer, GLYPH_CACHE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_glyph_cache_capset() { + assert_eq!(GLYPH_CACHE_BUFFER.len(), GLYPH_CACHE.size()); +} + +#[test] +fn from_buffer_correctly_parses_cache_definition() { + assert_eq!(*CACHE_DEFINITION, decode(CACHE_DEFINITION_BUFFER.as_ref()).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_cache_definition() { + let cache_def = *CACHE_DEFINITION; + + let buffer = encode_vec(&cache_def).unwrap(); + + assert_eq!(buffer, CACHE_DEFINITION_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_cache_definition() { + assert_eq!(CACHE_DEFINITION_BUFFER.len(), CACHE_DEFINITION.size()); +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/input.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/input.rs new file mode 100644 index 00000000..51fb0dfc --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/input.rs @@ -0,0 +1,109 @@ +#[cfg(test)] +mod tests; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, read_padding, write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; +use num_traits::FromPrimitive as _; + +use crate::gcc::{KeyboardType, IME_FILE_NAME_SIZE}; +use crate::utils; + +const INPUT_LENGTH: usize = 84; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct InputFlags: u16 { + const SCANCODES = 0x0001; + const MOUSEX = 0x0004; + const FASTPATH_INPUT = 0x0008; + const UNICODE = 0x0010; + const FASTPATH_INPUT_2 = 0x0020; + const UNUSED_1 = 0x0040; + const MOUSE_RELATIVE = 0x0080; + const TS_MOUSE_HWHEEL = 0x0100; + const TS_QOE_TIMESTAMPS = 0x0200; + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Input { + pub input_flags: InputFlags, + pub keyboard_layout: u32, + pub keyboard_type: Option, + pub keyboard_subtype: u32, + pub keyboard_function_key: u32, + pub keyboard_ime_filename: String, +} + +impl Input { + const NAME: &'static str = "Input"; + + const FIXED_PART_SIZE: usize = INPUT_LENGTH; +} + +impl Encode for Input { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.input_flags.bits()); + write_padding!(dst, 2); + dst.write_u32(self.keyboard_layout); + + let type_buffer = match self.keyboard_type.as_ref() { + Some(value) => value.as_u32(), + None => 0, + }; + dst.write_u32(type_buffer); + + dst.write_u32(self.keyboard_subtype); + dst.write_u32(self.keyboard_function_key); + + utils::encode_string( + dst.remaining_mut(), + &self.keyboard_ime_filename, + utils::CharacterSet::Unicode, + true, + )?; + dst.advance(IME_FILE_NAME_SIZE); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Input { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let input_flags = InputFlags::from_bits_truncate(src.read_u16()); + read_padding!(src, 2); + let keyboard_layout = src.read_u32(); + + let keyboard_type = KeyboardType::from_u32(src.read_u32()); + + let keyboard_subtype = src.read_u32(); + let keyboard_function_key = src.read_u32(); + + let keyboard_ime_filename = + utils::decode_string(src.read_slice(IME_FILE_NAME_SIZE), utils::CharacterSet::Unicode, false)?; + + Ok(Input { + input_flags, + keyboard_layout, + keyboard_type, + keyboard_subtype, + keyboard_function_key, + keyboard_ime_filename, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/input/tests.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/input/tests.rs new file mode 100644 index 00000000..5337365d --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/input/tests.rs @@ -0,0 +1,46 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const INPUT_BUFFER: [u8; 84] = [ + 0x15, 0x00, // inputFlags + 0x00, 0x00, // pad2octetsA + 0x09, 0x04, 0x00, 0x00, // keyboardLayout + 0x04, 0x00, 0x00, 0x00, // keyboardType + 0x00, 0x00, 0x00, 0x00, // keyboardSubType + 0x0c, 0x00, 0x00, 0x00, // keyboardFunctionKey + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // imeFileName +]; + +static INPUT: LazyLock = LazyLock::new(|| Input { + input_flags: InputFlags::SCANCODES | InputFlags::UNICODE | InputFlags::MOUSEX, + keyboard_layout: 0x409, + keyboard_type: Some(KeyboardType::IbmEnhanced), + keyboard_subtype: 0, + keyboard_function_key: 12, + keyboard_ime_filename: String::new(), +}); + +#[test] +fn from_buffer_correctly_parses_input_capset() { + assert_eq!(*INPUT, decode(INPUT_BUFFER.as_ref()).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_input_capset() { + let input = INPUT.clone(); + + let buffer = encode_vec(&input).unwrap(); + + assert_eq!(buffer, INPUT_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_input_capset() { + assert_eq!(INPUT_BUFFER.len(), INPUT.size()); +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/large_pointer.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/large_pointer.rs new file mode 100644 index 00000000..14c7ad92 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/large_pointer.rs @@ -0,0 +1,79 @@ +use bitflags::bitflags; +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct LargePointer { + pub flags: LargePointerSupportFlags, +} + +impl LargePointer { + const NAME: &'static str = "LargePointer"; + + const FIXED_PART_SIZE: usize = 2; +} + +impl Encode for LargePointer { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.flags.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for LargePointer { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = LargePointerSupportFlags::from_bits_truncate(src.read_u16()); + + Ok(Self { flags }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct LargePointerSupportFlags: u16 { + const UP_TO_96X96_PIXELS = 1; + const UP_TO_384X384_PIXELS = 2; + } +} + +#[cfg(test)] +mod test { + use ironrdp_core::{decode, encode_vec}; + + use super::*; + + const LARGE_POINTER_PDU_BUFFER: [u8; 2] = [0x01, 0x00]; + const LARGE_POINTER_PDU: LargePointer = LargePointer { + flags: LargePointerSupportFlags::UP_TO_96X96_PIXELS, + }; + + #[test] + fn from_buffer_correctly_parses_large_pointer() { + assert_eq!(LARGE_POINTER_PDU, decode(LARGE_POINTER_PDU_BUFFER.as_ref()).unwrap()); + } + + #[test] + fn to_buffer_correctly_serializes_large_pointer() { + let expected = LARGE_POINTER_PDU_BUFFER.as_ref(); + + let buffer = encode_vec(&LARGE_POINTER_PDU).unwrap(); + assert_eq!(expected, buffer.as_slice()); + } + + #[test] + fn buffer_length_is_correct_for_large_pointer() { + assert_eq!(LARGE_POINTER_PDU_BUFFER.len(), LARGE_POINTER_PDU.size()); + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/mod.rs new file mode 100644 index 00000000..2662e95b --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/mod.rs @@ -0,0 +1,673 @@ +use std::io; + +use ironrdp_core::{ + cast_length, decode, ensure_fixed_part_size, ensure_size, invalid_field_err, unsupported_value_err, write_padding, + Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; +use thiserror::Error; + +use crate::{utils, PduError}; + +mod bitmap; +mod bitmap_cache; +mod bitmap_codecs; +mod brush; +mod frame_acknowledge; +mod general; +mod glyph_cache; +mod input; +mod large_pointer; +mod multifragment_update; +mod offscreen_bitmap_cache; +mod order; +mod pointer; +mod sound; +mod surface_commands; +mod virtual_channel; + +pub use self::bitmap::{Bitmap, BitmapDrawingFlags}; +pub use self::bitmap_cache::{ + BitmapCache, BitmapCacheRev2, CacheEntry, CacheFlags, CellInfo, BITMAP_CACHE_ENTRIES_NUM, +}; +pub use self::bitmap_codecs::{ + client_codecs_capabilities, server_codecs_capabilities, BitmapCodecs, CaptureFlags, Codec, CodecId, CodecProperty, + EntropyBits, Guid, NsCodec, RemoteFxContainer, RfxCaps, RfxCapset, RfxClientCapsContainer, RfxICap, RfxICapFlags, + CODEC_ID_NONE, CODEC_ID_QOI, CODEC_ID_QOIZ, CODEC_ID_REMOTEFX, +}; +pub use self::brush::{Brush, SupportLevel}; +pub use self::frame_acknowledge::FrameAcknowledge; +pub use self::general::{General, GeneralExtraFlags, MajorPlatformType, MinorPlatformType, PROTOCOL_VER}; +pub use self::glyph_cache::{CacheDefinition, GlyphCache, GlyphSupportLevel, GLYPH_CACHE_NUM}; +pub use self::input::{Input, InputFlags}; +pub use self::large_pointer::{LargePointer, LargePointerSupportFlags}; +pub use self::multifragment_update::MultifragmentUpdate; +pub use self::offscreen_bitmap_cache::OffscreenBitmapCache; +pub use self::order::{Order, OrderFlags, OrderSupportExFlags, OrderSupportIndex}; +pub use self::pointer::Pointer; +pub use self::sound::{Sound, SoundFlags}; +pub use self::surface_commands::{CmdFlags, SurfaceCommands}; +pub use self::virtual_channel::{VirtualChannel, VirtualChannelFlags}; + +pub const SERVER_CHANNEL_ID: u16 = 0x03ea; + +const SOURCE_DESCRIPTOR_LENGTH_FIELD_SIZE: usize = 2; +const COMBINED_CAPABILITIES_LENGTH_FIELD_SIZE: usize = 2; +const NUMBER_CAPABILITIES_FIELD_SIZE: usize = 2; +const PADDING_SIZE: usize = 2; +const SESSION_ID_FIELD_SIZE: usize = 4; +const CAPABILITY_SET_TYPE_FIELD_SIZE: usize = 2; +const CAPABILITY_SET_LENGTH_FIELD_SIZE: usize = 2; +const ORIGINATOR_ID_FIELD_SIZE: usize = 2; + +const NULL_TERMINATOR: &str = "\0"; + +/// [2.2.1.13.1] Server Demand Active PDU +/// +/// [2.2.1.13.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/a07abad1-38bb-4a1a-96c9-253e3d5440df +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerDemandActive { + pub pdu: DemandActive, +} + +impl ServerDemandActive { + const NAME: &'static str = "ServerDemandActive"; + + const FIXED_PART_SIZE: usize = SESSION_ID_FIELD_SIZE; +} + +impl Encode for ServerDemandActive { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.pdu.encode(dst)?; + dst.write_u32(0); // This field is ignored by the client + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.pdu.size() + } +} + +impl<'de> Decode<'de> for ServerDemandActive { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let pdu = DemandActive::decode(src)?; + + ensure_size!(in: src, size: 4); + let _session_id = src.read_u32(); + + Ok(Self { pdu }) + } +} + +/// [2.2.1.13.2] Client Confirm Active PDU +/// +/// [2.2.1.13.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/4c3c2710-0bf0-4c54-8e69-aff40ffcde66 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientConfirmActive { + /// According to [MSDN](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/4e9722c3-ad83-43f5-af5a-529f73d88b48), + /// this field MUST be set to [SERVER_CHANNEL_ID](constant.SERVER_CHANNEL_ID.html). + /// However, the Microsoft RDP client takes this value from a server's + /// [PduSource](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/73d01865-2eae-407f-9b2c-87e31daac471) + /// field of the [Server Demand Active PDU](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/bd612af5-cb54-43a2-9646-438bc3ecf5db). + /// Therefore, checking the `originator_id` field is the responsibility of the user of the library. + pub originator_id: u16, + pub pdu: DemandActive, +} + +impl ClientConfirmActive { + const NAME: &'static str = "ClientConfirmActive"; + + const FIXED_PART_SIZE: usize = ORIGINATOR_ID_FIELD_SIZE; +} + +impl Encode for ClientConfirmActive { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.originator_id); + + self.pdu.encode(dst) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.pdu.size() + } +} + +impl<'de> Decode<'de> for ClientConfirmActive { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let originator_id = src.read_u16(); + let pdu = DemandActive::decode(src)?; + + Ok(Self { originator_id, pdu }) + } +} + +/// 2.2.1.13.1.1 Demand Active PDU Data (TS_DEMAND_ACTIVE_PDU) +/// +/// [2.2.1.13.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/bd612af5-cb54-43a2-9646-438bc3ecf5db +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DemandActive { + pub source_descriptor: String, + pub capability_sets: Vec, +} + +impl DemandActive { + const NAME: &'static str = "DemandActive"; + + const FIXED_PART_SIZE: usize = SOURCE_DESCRIPTOR_LENGTH_FIELD_SIZE + COMBINED_CAPABILITIES_LENGTH_FIELD_SIZE; +} + +impl Encode for DemandActive { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let combined_length = self.capability_sets.iter().map(Encode::size).sum::() + + NUMBER_CAPABILITIES_FIELD_SIZE + + PADDING_SIZE; + + dst.write_u16(cast_length!( + "sourceDescLen", + self.source_descriptor.len() + NULL_TERMINATOR.len() + )?); + dst.write_u16(cast_length!("combinedLen", combined_length)?); + dst.write_slice(self.source_descriptor.as_ref()); + dst.write_slice(NULL_TERMINATOR.as_bytes()); + dst.write_u16(cast_length!("len", self.capability_sets.len())?); + write_padding!(dst, 2); + + for capability_set in self.capability_sets.iter() { + capability_set.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + self.source_descriptor.len() + + 1 + + NUMBER_CAPABILITIES_FIELD_SIZE + + PADDING_SIZE + + self.capability_sets.iter().map(Encode::size).sum::() + } +} + +impl<'de> Decode<'de> for DemandActive { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let source_descriptor_length = usize::from(src.read_u16()); + // The combined size in bytes of the numberCapabilities, pad2Octets, and capabilitySets fields. + let _combined_capabilities_length = usize::from(src.read_u16()); + + ensure_size!(in: src, size: source_descriptor_length); + let source_descriptor = utils::decode_string( + src.read_slice(source_descriptor_length), + utils::CharacterSet::Ansi, + false, + )?; + + ensure_size!(in: src, size: 2 + 2); + let capability_sets_count = usize::from(src.read_u16()); + let _padding = src.read_u16(); + + let mut capability_sets = Vec::with_capacity(capability_sets_count); + + for _ in 0..capability_sets_count { + capability_sets.push(CapabilitySet::decode(src)?); + } + + Ok(Self { + source_descriptor, + capability_sets, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CapabilitySet { + // mandatory + General(General), + Bitmap(Bitmap), + Order(Order), + BitmapCache(BitmapCache), + BitmapCacheRev2(BitmapCacheRev2), + Pointer(Pointer), + Sound(Sound), + Input(Input), + Brush(Brush), + GlyphCache(GlyphCache), + OffscreenBitmapCache(OffscreenBitmapCache), + VirtualChannel(VirtualChannel), + + // optional + Control(Vec), + WindowActivation(Vec), + Share(Vec), + Font(Vec), + BitmapCacheHostSupport(Vec), + DesktopComposition(Vec), + MultiFragmentUpdate(MultifragmentUpdate), + LargePointer(LargePointer), + SurfaceCommands(SurfaceCommands), + BitmapCodecs(BitmapCodecs), + + // other + FrameAcknowledge(FrameAcknowledge), + ColorCache(Vec), + DrawNineGridCache(Vec), + DrawGdiPlus(Vec), + Rail(Vec), + WindowList(Vec), + BitmapCacheV3(Vec), +} + +impl CapabilitySet { + const NAME: &'static str = "CapabilitySet"; + + const FIXED_PART_SIZE: usize = CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE; +} + +impl Encode for CapabilitySet { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + match self { + CapabilitySet::General(capset) => { + dst.write_u16(CapabilitySetType::General.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::Bitmap(capset) => { + dst.write_u16(CapabilitySetType::Bitmap.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::Order(capset) => { + dst.write_u16(CapabilitySetType::Order.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::BitmapCache(capset) => { + dst.write_u16(CapabilitySetType::BitmapCache.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::BitmapCacheRev2(capset) => { + dst.write_u16(CapabilitySetType::BitmapCacheRev2.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::Pointer(capset) => { + dst.write_u16(CapabilitySetType::Pointer.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::Sound(capset) => { + dst.write_u16(CapabilitySetType::Sound.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::Input(capset) => { + dst.write_u16(CapabilitySetType::Input.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::Brush(capset) => { + dst.write_u16(CapabilitySetType::Brush.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::GlyphCache(capset) => { + dst.write_u16(CapabilitySetType::GlyphCache.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::OffscreenBitmapCache(capset) => { + dst.write_u16(CapabilitySetType::OffscreenBitmapCache.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::VirtualChannel(capset) => { + dst.write_u16(CapabilitySetType::VirtualChannel.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::SurfaceCommands(capset) => { + dst.write_u16(CapabilitySetType::SurfaceCommands.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::BitmapCodecs(capset) => { + dst.write_u16(CapabilitySetType::BitmapCodecs.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::MultiFragmentUpdate(capset) => { + dst.write_u16(CapabilitySetType::MultiFragmentUpdate.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::LargePointer(capset) => { + dst.write_u16(CapabilitySetType::LargePointer.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + CapabilitySet::FrameAcknowledge(capset) => { + dst.write_u16(CapabilitySetType::FrameAcknowledge.as_u16()); + dst.write_u16(cast_length!( + "len", + capset.size() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + capset.encode(dst)?; + } + _ => { + let (capability_set_type, capability_set_buffer) = match self { + CapabilitySet::Control(buffer) => (CapabilitySetType::Control, buffer), + CapabilitySet::WindowActivation(buffer) => (CapabilitySetType::WindowActivation, buffer), + CapabilitySet::Share(buffer) => (CapabilitySetType::Share, buffer), + CapabilitySet::Font(buffer) => (CapabilitySetType::Font, buffer), + CapabilitySet::BitmapCacheHostSupport(buffer) => { + (CapabilitySetType::BitmapCacheHostSupport, buffer) + } + CapabilitySet::DesktopComposition(buffer) => (CapabilitySetType::DesktopComposition, buffer), + CapabilitySet::ColorCache(buffer) => (CapabilitySetType::ColorCache, buffer), + CapabilitySet::DrawNineGridCache(buffer) => (CapabilitySetType::DrawNineGridCache, buffer), + CapabilitySet::DrawGdiPlus(buffer) => (CapabilitySetType::DrawGdiPlus, buffer), + CapabilitySet::Rail(buffer) => (CapabilitySetType::Rail, buffer), + CapabilitySet::WindowList(buffer) => (CapabilitySetType::WindowList, buffer), + _ => unreachable!(), + }; + + dst.write_u16(capability_set_type.as_u16()); + dst.write_u16(cast_length!( + "len", + capability_set_buffer.len() + CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE + )?); + dst.write_slice(capability_set_buffer); + } + }; + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + match self { + CapabilitySet::General(capset) => capset.size(), + CapabilitySet::Bitmap(capset) => capset.size(), + CapabilitySet::Order(capset) => capset.size(), + CapabilitySet::BitmapCache(capset) => capset.size(), + CapabilitySet::BitmapCacheRev2(capset) => capset.size(), + CapabilitySet::Pointer(capset) => capset.size(), + CapabilitySet::Sound(capset) => capset.size(), + CapabilitySet::Input(capset) => capset.size(), + CapabilitySet::Brush(capset) => capset.size(), + CapabilitySet::GlyphCache(capset) => capset.size(), + CapabilitySet::OffscreenBitmapCache(capset) => capset.size(), + CapabilitySet::VirtualChannel(capset) => capset.size(), + CapabilitySet::SurfaceCommands(capset) => capset.size(), + CapabilitySet::BitmapCodecs(capset) => capset.size(), + CapabilitySet::MultiFragmentUpdate(capset) => capset.size(), + CapabilitySet::LargePointer(capset) => capset.size(), + CapabilitySet::FrameAcknowledge(capset) => capset.size(), + CapabilitySet::Control(buffer) + | CapabilitySet::WindowActivation(buffer) + | CapabilitySet::Share(buffer) + | CapabilitySet::Font(buffer) + | CapabilitySet::BitmapCacheHostSupport(buffer) + | CapabilitySet::DesktopComposition(buffer) + | CapabilitySet::ColorCache(buffer) + | CapabilitySet::DrawNineGridCache(buffer) + | CapabilitySet::DrawGdiPlus(buffer) + | CapabilitySet::Rail(buffer) + | CapabilitySet::WindowList(buffer) + | CapabilitySet::BitmapCacheV3(buffer) => buffer.len(), + } + } +} + +impl<'de> Decode<'de> for CapabilitySet { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let capability_set_type_raw = src.read_u16(); + let capability_set_type = CapabilitySetType::from_u16(capability_set_type_raw).ok_or_else(|| { + unsupported_value_err!( + "capabilitySetType", + format!("invalid capability set type: {}", capability_set_type_raw) + ) + })?; + + let length = usize::from(src.read_u16()); + + if length < CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE { + return Err(invalid_field_err!("len", "invalid capability set length")); + } + + let buffer_length = length - CAPABILITY_SET_TYPE_FIELD_SIZE - CAPABILITY_SET_LENGTH_FIELD_SIZE; + ensure_size!(in: src, size: buffer_length); + let capability_set_buffer = src.read_slice(buffer_length); + + match capability_set_type { + CapabilitySetType::General => Ok(CapabilitySet::General(decode(capability_set_buffer)?)), + CapabilitySetType::Bitmap => Ok(CapabilitySet::Bitmap(decode(capability_set_buffer)?)), + CapabilitySetType::Order => Ok(CapabilitySet::Order(decode(capability_set_buffer)?)), + CapabilitySetType::BitmapCache => Ok(CapabilitySet::BitmapCache(decode(capability_set_buffer)?)), + CapabilitySetType::BitmapCacheRev2 => Ok(CapabilitySet::BitmapCacheRev2(decode(capability_set_buffer)?)), + CapabilitySetType::Pointer => Ok(CapabilitySet::Pointer(decode(capability_set_buffer)?)), + CapabilitySetType::Sound => Ok(CapabilitySet::Sound(decode(capability_set_buffer)?)), + CapabilitySetType::Input => Ok(CapabilitySet::Input(decode(capability_set_buffer)?)), + CapabilitySetType::Brush => Ok(CapabilitySet::Brush(decode(capability_set_buffer)?)), + CapabilitySetType::GlyphCache => Ok(CapabilitySet::GlyphCache(decode(capability_set_buffer)?)), + CapabilitySetType::OffscreenBitmapCache => { + Ok(CapabilitySet::OffscreenBitmapCache(decode(capability_set_buffer)?)) + } + CapabilitySetType::VirtualChannel => Ok(CapabilitySet::VirtualChannel(decode(capability_set_buffer)?)), + CapabilitySetType::SurfaceCommands => Ok(CapabilitySet::SurfaceCommands(decode(capability_set_buffer)?)), + CapabilitySetType::BitmapCodecs => Ok(CapabilitySet::BitmapCodecs(decode(capability_set_buffer)?)), + + CapabilitySetType::Control => Ok(CapabilitySet::Control(capability_set_buffer.into())), + CapabilitySetType::WindowActivation => Ok(CapabilitySet::WindowActivation(capability_set_buffer.into())), + CapabilitySetType::Share => Ok(CapabilitySet::Share(capability_set_buffer.into())), + CapabilitySetType::Font => Ok(CapabilitySet::Font(capability_set_buffer.into())), + CapabilitySetType::BitmapCacheHostSupport => { + Ok(CapabilitySet::BitmapCacheHostSupport(capability_set_buffer.into())) + } + CapabilitySetType::DesktopComposition => { + Ok(CapabilitySet::DesktopComposition(capability_set_buffer.into())) + } + CapabilitySetType::MultiFragmentUpdate => { + Ok(CapabilitySet::MultiFragmentUpdate(decode(capability_set_buffer)?)) + } + CapabilitySetType::LargePointer => Ok(CapabilitySet::LargePointer(decode(capability_set_buffer)?)), + CapabilitySetType::ColorCache => Ok(CapabilitySet::ColorCache(capability_set_buffer.into())), + CapabilitySetType::DrawNineGridCache => Ok(CapabilitySet::DrawNineGridCache(capability_set_buffer.into())), + CapabilitySetType::DrawGdiPlus => Ok(CapabilitySet::DrawGdiPlus(capability_set_buffer.into())), + CapabilitySetType::Rail => Ok(CapabilitySet::Rail(capability_set_buffer.into())), + CapabilitySetType::WindowList => Ok(CapabilitySet::WindowList(capability_set_buffer.into())), + CapabilitySetType::FrameAcknowledge => Ok(CapabilitySet::FrameAcknowledge(decode(capability_set_buffer)?)), + CapabilitySetType::BitmapCacheV3CodecID => Ok(CapabilitySet::BitmapCacheV3(capability_set_buffer.into())), + } + } +} + +#[repr(u16)] +#[derive(Copy, Clone, Debug, FromPrimitive)] +enum CapabilitySetType { + General = 0x01, + Bitmap = 0x02, + Order = 0x03, + BitmapCache = 0x04, + Control = 0x05, + BitmapCacheV3CodecID = 0x06, + WindowActivation = 0x07, + Pointer = 0x08, + Share = 0x09, + ColorCache = 0x0a, + Sound = 0x0c, + Input = 0x0d, + Font = 0x0e, + Brush = 0x0f, + GlyphCache = 0x10, + OffscreenBitmapCache = 0x11, + BitmapCacheHostSupport = 0x12, + BitmapCacheRev2 = 0x13, + VirtualChannel = 0x14, + DrawNineGridCache = 0x15, + DrawGdiPlus = 0x16, + Rail = 0x17, + WindowList = 0x18, + DesktopComposition = 0x19, + MultiFragmentUpdate = 0x1a, + LargePointer = 0x1b, + SurfaceCommands = 0x1c, + BitmapCodecs = 0x1d, + FrameAcknowledge = 0x1e, +} + +impl CapabilitySetType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[derive(Debug, Error)] +pub enum CapabilitySetsError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("UTF-8 error")] + Utf8Error(#[from] std::string::FromUtf8Error), + #[error("invalid type field")] + InvalidType, + #[error("invalid bitmap compression field")] + InvalidCompressionFlag, + #[error("invalid multiple rectangle support field")] + InvalidMultipleRectSupport, + #[error("invalid protocol version field")] + InvalidProtocolVersion, + #[error("invalid compression types field")] + InvalidCompressionTypes, + #[error("invalid update capability flags field")] + InvalidUpdateCapFlag, + #[error("invalid remote unshare flag field")] + InvalidRemoteUnshareFlag, + #[error("invalid compression level field")] + InvalidCompressionLevel, + #[error("invalid brush support level field")] + InvalidBrushSupportLevel, + #[error("invalid glyph support level field")] + InvalidGlyphSupportLevel, + #[error("invalid RemoteFX capability version")] + InvalidRfxICapVersion, + #[error("invalid RemoteFX capability tile size")] + InvalidRfxICapTileSize, + #[error("invalid RemoteFXICap color conversion bits")] + InvalidRfxICapColorConvBits, + #[error("invalid RemoteFXICap transform bits")] + InvalidRfxICapTransformBits, + #[error("invalid RemoteFXICap entropy bits field")] + InvalidRfxICapEntropyBits, + #[error("invalid RemoteFX capability set block type")] + InvalidRfxCapsetBlockType, + #[error("invalid RemoteFX capability set type")] + InvalidRfxCapsetType, + #[error("invalid RemoteFX capabilities block type")] + InvalidRfxCapsBlockType, + #[error("invalid RemoteFX capabilities block length")] + InvalidRfxCapsBockLength, + #[error("invalid number of capability sets in RemoteFX capabilities")] + InvalidRfxCapsNumCapsets, + #[error("invalid codec property field")] + InvalidCodecProperty, + #[error("invalid codec ID")] + InvalidCodecID, + #[error("invalid channel chunk size field")] + InvalidChunkSize, + #[error("invalid codec property length for the current property ID")] + InvalidPropertyLength, + #[error("invalid data length")] + InvalidLength, + #[error("PDU error: {0}")] + Pdu(PduError), +} + +impl From for CapabilitySetsError { + fn from(e: PduError) -> Self { + Self::Pdu(e) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/multifragment_update.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/multifragment_update.rs new file mode 100644 index 00000000..578d0532 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/multifragment_update.rs @@ -0,0 +1,73 @@ +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct MultifragmentUpdate { + pub max_request_size: u32, +} + +impl MultifragmentUpdate { + const NAME: &'static str = "MultifragmentUpdate"; + + const FIXED_PART_SIZE: usize = 4; +} + +impl Encode for MultifragmentUpdate { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.max_request_size); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for MultifragmentUpdate { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let max_request_size = src.read_u32(); + + Ok(Self { max_request_size }) + } +} + +#[cfg(test)] +mod test { + use ironrdp_core::{decode, encode_vec}; + + use super::*; + + const MULTIFRAGMENT_UPDATE_PDU_BUFFER: [u8; 4] = [0xf4, 0xf3, 0xf2, 0xf1]; + const MULTIFRAGMENT_UPDATE_PDU: MultifragmentUpdate = MultifragmentUpdate { + max_request_size: 0xf1f2_f3f4, + }; + + #[test] + fn from_buffer_correctly_parses_multifragment_update() { + assert_eq!( + MULTIFRAGMENT_UPDATE_PDU, + decode(MULTIFRAGMENT_UPDATE_PDU_BUFFER.as_ref()).unwrap() + ); + } + + #[test] + fn to_buffer_correctly_serializes_multifragment_update() { + let expected = MULTIFRAGMENT_UPDATE_PDU_BUFFER.as_ref(); + + let buffer = encode_vec(&MULTIFRAGMENT_UPDATE_PDU).unwrap(); + assert_eq!(expected, buffer.as_slice()); + } + + #[test] + fn buffer_length_is_correct_for_multifragment_update() { + assert_eq!(MULTIFRAGMENT_UPDATE_PDU_BUFFER.len(), MULTIFRAGMENT_UPDATE_PDU.size()); + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/offscreen_bitmap_cache/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/offscreen_bitmap_cache/mod.rs new file mode 100644 index 00000000..25271c84 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/offscreen_bitmap_cache/mod.rs @@ -0,0 +1,55 @@ +#[cfg(test)] +mod tests; + +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +const OFFSCREEN_BITMAP_CACHE_LENGTH: usize = 8; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct OffscreenBitmapCache { + pub is_supported: bool, + pub cache_size: u16, + pub cache_entries: u16, +} + +impl OffscreenBitmapCache { + const NAME: &'static str = "OffscreenBitmapCache"; + + const FIXED_PART_SIZE: usize = OFFSCREEN_BITMAP_CACHE_LENGTH; +} + +impl Encode for OffscreenBitmapCache { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(u32::from(self.is_supported)); + dst.write_u16(self.cache_size); + dst.write_u16(self.cache_entries); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for OffscreenBitmapCache { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let is_supported = src.read_u32() != 0; + let cache_size = src.read_u16(); + let cache_entries = src.read_u16(); + + Ok(OffscreenBitmapCache { + is_supported, + cache_size, + cache_entries, + }) + } +} diff --git a/ironrdp/src/rdp/capability_sets/offscreen_bitmap_cache/test.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/offscreen_bitmap_cache/tests.rs similarity index 51% rename from ironrdp/src/rdp/capability_sets/offscreen_bitmap_cache/test.rs rename to crates/ironrdp-pdu/src/rdp/capability_sets/offscreen_bitmap_cache/tests.rs index 503f2219..e8dac691 100644 --- a/ironrdp/src/rdp/capability_sets/offscreen_bitmap_cache/test.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/offscreen_bitmap_cache/tests.rs @@ -1,4 +1,6 @@ -use lazy_static::lazy_static; +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; use super::*; @@ -7,36 +9,30 @@ const OFFSCREEN_BITMAP_CACHE_BUFFER: [u8; 8] = [ 0x00, 0x1e, // offscreenCacheSize 0x64, 0x00, // offscreenCacheEntries ]; - -lazy_static! { - pub static ref OFFSCREEN_BITMAP_CACHE: OffscreenBitmapCache = OffscreenBitmapCache { - is_supported: true, - cache_size: 7680, - cache_entries: 100, - }; -} +static OFFSCREEN_BITMAP_CACHE: LazyLock = LazyLock::new(|| OffscreenBitmapCache { + is_supported: true, + cache_size: 7680, + cache_entries: 100, +}); #[test] fn from_buffer_correctly_parses_offscreen_bitmap_cache_capset() { assert_eq!( *OFFSCREEN_BITMAP_CACHE, - OffscreenBitmapCache::from_buffer(OFFSCREEN_BITMAP_CACHE_BUFFER.as_ref()).unwrap() + decode(OFFSCREEN_BITMAP_CACHE_BUFFER.as_ref()).unwrap() ); } #[test] fn to_buffer_correctly_serializes_offscreen_bitmap_cache_capset() { - let mut buffer = Vec::new(); + let off = OFFSCREEN_BITMAP_CACHE.clone(); - OFFSCREEN_BITMAP_CACHE.to_buffer(&mut buffer).unwrap(); + let buffer = encode_vec(&off).unwrap(); assert_eq!(buffer, OFFSCREEN_BITMAP_CACHE_BUFFER.as_ref()); } #[test] fn buffer_length_is_correct_for_offscreen_bitmap_cache_capset() { - assert_eq!( - OFFSCREEN_BITMAP_CACHE_BUFFER.len(), - OFFSCREEN_BITMAP_CACHE.buffer_length() - ); + assert_eq!(OFFSCREEN_BITMAP_CACHE_BUFFER.len(), OFFSCREEN_BITMAP_CACHE.size()); } diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/order/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/order/mod.rs new file mode 100644 index 00000000..70d823cb --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/order/mod.rs @@ -0,0 +1,174 @@ +#[cfg(test)] +mod tests; + +use bitflags::bitflags; +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +const ORDER_LENGTH: usize = 84; +const ORD_LEVEL_1_ORDERS: u16 = 1; +const SUPPORT_ARRAY_LEN: usize = 32; +const DESKTOP_SAVE_Y_GRAN_VAL: u16 = 20; + +#[repr(u8)] +#[derive(Copy, Clone)] +pub enum OrderSupportIndex { + DstBlt = 0x00, + PatBlt = 0x01, + ScrBlt = 0x02, + MemBlt = 0x03, + Mem3Blt = 0x04, + DrawnInEGrid = 0x07, + LineTo = 0x08, + MultiDrawnInEGrid = 0x09, + SaveBitmap = 0x0B, + MultiDstBlt = 0x0F, + MultiPatBlt = 0x10, + MultiScrBlt = 0x11, + MultiOpaqueRect = 0x12, + Fast = 0x13, + PolygonSC = 0x14, + PolygonCB = 0x15, + Polyline = 0x16, + FastGlyph = 0x18, + EllipseSC = 0x19, + EllipseCB = 0x1A, + Index = 0x1B, +} + +impl OrderSupportIndex { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct OrderFlags: u16 { + const NEGOTIATE_ORDER_SUPPORT = 0x0002; + const ZERO_BOUNDS_DELTAS_SUPPORT = 0x0008; + const COLOR_INDEX_SUPPORT = 0x0020; + const SOLID_PATTERN_BRUSH_ONLY = 0x0040; + const ORDER_FLAGS_EXTRA_FLAGS = 0x0080; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct OrderSupportExFlags: u16 { + const CACHE_BITMAP_REV3_SUPPORT = 2; + const ALTSEC_FRAME_MARKER_SUPPORT = 4; + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Order { + order_flags: OrderFlags, + order_support: [u8; SUPPORT_ARRAY_LEN], + order_support_ex_flags: OrderSupportExFlags, + desktop_save_size: u32, + text_ansi_code_page: u16, +} + +impl Order { + const NAME: &'static str = "Order"; + + const FIXED_PART_SIZE: usize = ORDER_LENGTH; + + pub fn new( + order_flags: OrderFlags, + order_support_ex_flags: OrderSupportExFlags, + desktop_save_size: u32, + text_ansi_code_page: u16, + ) -> Self { + Self { + order_flags, + order_support: [0; SUPPORT_ARRAY_LEN], + order_support_ex_flags, + desktop_save_size, + text_ansi_code_page, + } + } + + pub fn set_support_flag(&mut self, flag: OrderSupportIndex, value: bool) { + self.order_support[usize::from(flag.as_u8())] = u8::from(value) + } + + pub fn get_support_flag(&mut self, flag: OrderSupportIndex) -> bool { + self.order_support[usize::from(flag.as_u8())] == 1 + } +} + +impl Encode for Order { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u128(0); + + dst.write_u32(0); // padding + dst.write_u16(1); // desktopSaveXGranularity + dst.write_u16(DESKTOP_SAVE_Y_GRAN_VAL); + dst.write_u16(0); // padding + dst.write_u16(ORD_LEVEL_1_ORDERS); // maximumOrderLevel + dst.write_u16(0); // numberFonts + dst.write_u16(self.order_flags.bits()); + dst.write_slice(&self.order_support); + dst.write_u16(0); // textFlags + dst.write_u16(self.order_support_ex_flags.bits()); + dst.write_u32(0); // padding + dst.write_u32(self.desktop_save_size); + dst.write_u16(0); // padding + dst.write_u16(0); // padding + dst.write_u16(self.text_ansi_code_page); + dst.write_u16(0); // padding + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Order { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _terminal_descriptor = src.read_u128(); + let _padding = src.read_u32(); + let _desktop_save_x_granularity = src.read_u16(); + let _desktop_save_y_granularity = src.read_u16(); + let _padding = src.read_u16(); + let _max_order_level = src.read_u16(); + let _num_fonts = src.read_u16(); + + let order_flags = OrderFlags::from_bits_truncate(src.read_u16()); + let order_support = src.read_array(); + + let _text_flags = src.read_u16(); + + let order_support_ex_flags = OrderSupportExFlags::from_bits_truncate(src.read_u16()); + + let _padding = src.read_u32(); + let desktop_save_size = src.read_u32(); + let _padding = src.read_u16(); + let _padding = src.read_u16(); + let text_ansi_code_page = src.read_u16(); + let _padding = src.read_u16(); + + Ok(Order { + order_flags, + order_support, + order_support_ex_flags, + desktop_save_size, + text_ansi_code_page, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/order/tests.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/order/tests.rs new file mode 100644 index 00000000..85d0e2ee --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/order/tests.rs @@ -0,0 +1,84 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const ORDER_BUFFER: [u8; 84] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // pad4octetsA + 0x01, 0x00, // desktopSaveXGranularity + 0x14, 0x00, // desktopSaveYGranularity + 0x00, 0x00, // pad2octetsA + 0x01, 0x00, // maximumOrderLevel + 0x00, 0x00, // numberFonts + 0x22, 0x00, // orderFlags + 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, // orderSupport + 0x00, 0x00, // textFlags + 0x02, 0x00, // orderSupportExFlags + 0x00, 0x00, 0x00, 0x00, // pad4octetsB + 0x00, 0x84, 0x03, 0x00, // desktopSaveSize + 0x00, 0x00, // pad2octetsC + 0x00, 0x00, // pad2octetsD + 0x00, 0x00, // testANSICodePage + 0x00, 0x00, // pad2octetsE +]; + +static ORDER: LazyLock = LazyLock::new(|| Order { + order_flags: OrderFlags::COLOR_INDEX_SUPPORT | OrderFlags::NEGOTIATE_ORDER_SUPPORT, + order_support: { + let mut array = [0u8; 32]; + + array[usize::from(OrderSupportIndex::DstBlt.as_u8())] = 1; + array[usize::from(OrderSupportIndex::PatBlt.as_u8())] = 1; + array[usize::from(OrderSupportIndex::ScrBlt.as_u8())] = 1; + array[usize::from(OrderSupportIndex::MemBlt.as_u8())] = 1; + array[usize::from(OrderSupportIndex::Mem3Blt.as_u8())] = 1; + array[usize::from(OrderSupportIndex::DrawnInEGrid.as_u8())] = 1; + array[usize::from(OrderSupportIndex::LineTo.as_u8())] = 1; + array[usize::from(OrderSupportIndex::MultiDrawnInEGrid.as_u8())] = 1; + array[usize::from(OrderSupportIndex::SaveBitmap.as_u8())] = 1; + array[usize::from(OrderSupportIndex::MultiDstBlt.as_u8())] = 1; + array[usize::from(OrderSupportIndex::MultiPatBlt.as_u8())] = 1; + array[usize::from(OrderSupportIndex::MultiScrBlt.as_u8())] = 1; + array[usize::from(OrderSupportIndex::MultiOpaqueRect.as_u8())] = 1; + array[usize::from(OrderSupportIndex::Fast.as_u8())] = 1; + array[usize::from(OrderSupportIndex::PolygonSC.as_u8())] = 1; + array[usize::from(OrderSupportIndex::PolygonCB.as_u8())] = 1; + array[usize::from(OrderSupportIndex::Polyline.as_u8())] = 1; + array[usize::from(OrderSupportIndex::FastGlyph.as_u8())] = 1; + array[usize::from(OrderSupportIndex::EllipseSC.as_u8())] = 1; + array[usize::from(OrderSupportIndex::EllipseCB.as_u8())] = 1; + array[usize::from(OrderSupportIndex::Index.as_u8())] = 1; + + array + }, + + order_support_ex_flags: OrderSupportExFlags::CACHE_BITMAP_REV3_SUPPORT, + desktop_save_size: 230_400, + text_ansi_code_page: 0, +}); + +#[test] +fn from_buffer_correctly_parses_order_capset() { + let buffer = ORDER_BUFFER.as_ref(); + + assert_eq!(*ORDER, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_order_capset() { + let capset = ORDER.clone(); + + let buffer = encode_vec(&capset).unwrap(); + + assert_eq!(buffer, ORDER_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_order_capset() { + let correct_buffer_length = ORDER_BUFFER.len(); + + assert_eq!(correct_buffer_length, ORDER.size()); +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/pointer.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/pointer.rs new file mode 100644 index 00000000..5c4d9842 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/pointer.rs @@ -0,0 +1,53 @@ +#[cfg(test)] +mod tests; + +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +const POINTER_LENGTH: usize = 6; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Pointer { + pub color_pointer_cache_size: u16, + pub pointer_cache_size: u16, +} + +impl Pointer { + const NAME: &'static str = "Pointer"; + + const FIXED_PART_SIZE: usize = POINTER_LENGTH; +} + +impl Encode for Pointer { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(1); // color pointer flag + dst.write_u16(self.color_pointer_cache_size); + dst.write_u16(self.pointer_cache_size); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Pointer { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _color_pointer_flag = src.read_u16() != 0; + let color_pointer_cache_size = src.read_u16(); + let pointer_cache_size = src.read_u16(); + + Ok(Pointer { + color_pointer_cache_size, + pointer_cache_size, + }) + } +} diff --git a/ironrdp/src/rdp/capability_sets/pointer/test.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/pointer/tests.rs similarity index 55% rename from ironrdp/src/rdp/capability_sets/pointer/test.rs rename to crates/ironrdp-pdu/src/rdp/capability_sets/pointer/tests.rs index c74114fc..f7b0a73a 100644 --- a/ironrdp/src/rdp/capability_sets/pointer/test.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/pointer/tests.rs @@ -1,4 +1,6 @@ -use lazy_static::lazy_static; +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; use super::*; @@ -8,27 +10,23 @@ const POINTER_BUFFER: [u8; 6] = [ 0x15, 0x00, // pointerCacheSize ]; -lazy_static! { - pub static ref POINTER: Pointer = Pointer { - color_pointer_cache_size: 20, - pointer_cache_size: 21, - }; -} +static POINTER: LazyLock = LazyLock::new(|| Pointer { + color_pointer_cache_size: 20, + pointer_cache_size: 21, +}); #[test] fn from_buffer_correctly_parses_pointer_capset() { let buffer = POINTER_BUFFER.as_ref(); - assert_eq!(*POINTER, Pointer::from_buffer(buffer).unwrap()); + assert_eq!(*POINTER, decode(buffer).unwrap()); } #[test] fn to_buffer_correctly_serializes_pointer_capset() { - let mut buffer: Vec = Vec::new(); + let capset = POINTER.clone(); - let capset = &POINTER; - - capset.to_buffer(&mut buffer).unwrap(); + let buffer = encode_vec(&capset).unwrap(); assert_eq!(buffer, POINTER_BUFFER.as_ref()); } @@ -37,5 +35,5 @@ fn to_buffer_correctly_serializes_pointer_capset() { fn buffer_length_is_correct_for_pointer_capset() { let correct_length = POINTER_BUFFER.len(); - assert_eq!(correct_length, POINTER.buffer_length()); + assert_eq!(correct_length, POINTER.size()); } diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/sound/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/sound/mod.rs new file mode 100644 index 00000000..b4ca1e67 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/sound/mod.rs @@ -0,0 +1,58 @@ +#[cfg(test)] +mod tests; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, read_padding, write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +const SOUND_LENGTH: usize = 4; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct SoundFlags: u16 { + const BEEPS = 1; + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Sound { + pub flags: SoundFlags, +} + +impl Sound { + const NAME: &'static str = "Sound"; + + const FIXED_PART_SIZE: usize = SOUND_LENGTH; +} + +impl Encode for Sound { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.flags.bits()); + write_padding!(dst, 2); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Sound { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = SoundFlags::from_bits_truncate(src.read_u16()); + read_padding!(src, 2); + + Ok(Sound { flags }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/sound/tests.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/sound/tests.rs new file mode 100644 index 00000000..d43f52b0 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/sound/tests.rs @@ -0,0 +1,30 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const SOUND_BUFFER: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; + +static SOUND: LazyLock = LazyLock::new(|| Sound { + flags: SoundFlags::BEEPS, +}); + +#[test] +fn from_buffer_correctly_parses_sound_capset() { + assert_eq!(*SOUND, decode(SOUND_BUFFER.as_ref()).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_sound_capset() { + let sound = SOUND.clone(); + + let buffer = encode_vec(&sound).unwrap(); + + assert_eq!(buffer, SOUND_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_sound_capset() { + assert_eq!(SOUND.size(), SOUND_BUFFER.len()); +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/surface_commands.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/surface_commands.rs new file mode 100644 index 00000000..c33cb178 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/surface_commands.rs @@ -0,0 +1,57 @@ +#[cfg(test)] +mod tests; + +use bitflags::bitflags; +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +const SURFACE_COMMANDS_LENGTH: usize = 8; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CmdFlags: u32 { + const SET_SURFACE_BITS = 0x02; + const FRAME_MARKER = 0x10; + const STREAM_SURFACE_BITS = 0x40; + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SurfaceCommands { + pub flags: CmdFlags, +} + +impl SurfaceCommands { + const NAME: &'static str = "SurfaceCommands"; + + const FIXED_PART_SIZE: usize = SURFACE_COMMANDS_LENGTH; +} + +impl Encode for SurfaceCommands { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.flags.bits()); + dst.write_u32(0); // reserved + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for SurfaceCommands { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = CmdFlags::from_bits_truncate(src.read_u32()); + let _reserved = src.read_u32(); + + Ok(SurfaceCommands { flags }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/surface_commands/tests.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/surface_commands/tests.rs new file mode 100644 index 00000000..ac96f46e --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/surface_commands/tests.rs @@ -0,0 +1,33 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const SURFACE_COMMANDS_BUFFER: [u8; 8] = [ + 0x52, 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // reserved +]; + +static SURFACE_COMMANDS: LazyLock = LazyLock::new(|| SurfaceCommands { + flags: CmdFlags::SET_SURFACE_BITS | CmdFlags::FRAME_MARKER | CmdFlags::STREAM_SURFACE_BITS, +}); + +#[test] +fn from_buffer_correctly_parses_surface_commands_capset() { + assert_eq!(*SURFACE_COMMANDS, decode(SURFACE_COMMANDS_BUFFER.as_ref()).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_surface_commands_capset() { + let surf = SURFACE_COMMANDS.clone(); + + let buffer = encode_vec(&surf).unwrap(); + + assert_eq!(buffer, SURFACE_COMMANDS_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_surface_commands_capset() { + assert_eq!(SURFACE_COMMANDS_BUFFER.len(), SURFACE_COMMANDS.size()); +} diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/virtual_channel/mod.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/virtual_channel/mod.rs new file mode 100644 index 00000000..085e78f9 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/virtual_channel/mod.rs @@ -0,0 +1,90 @@ +#[cfg(test)] +mod tests; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, ensure_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; + +const FLAGS_FIELD_SIZE: usize = 4; +const CHUNK_SIZE_FIELD_SIZE: usize = 4; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct VirtualChannelFlags: u32 { + const NO_COMPRESSION = 0; + const COMPRESSION_SERVER_TO_CLIENT = 1; + const COMPRESSION_CLIENT_TO_SERVER_8K = 2; + } +} + +/// The VirtualChannel structure is used to advertise virtual channel support characteristics. This capability is sent by both client and server. +/// +/// # Fields +/// +/// * `flags` - virtual channel compression flags +/// * `chunk_size` - when sent from server to client, this field contains the maximum allowed size of a virtual channel chunk and MUST be greater than or equal to 1600 and less than or equal to 16256. +/// When sent from client to server, the value in this field is ignored by the server. This value is not verified in IronRDP and MUST be verified on the caller's side +/// +/// # MSDN +/// +/// * [Virtual Channel Capability Set](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/a8593178-80c0-4b80-876c-cb77e62cecfc) +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct VirtualChannel { + pub flags: VirtualChannelFlags, + pub chunk_size: Option, +} + +impl VirtualChannel { + const NAME: &'static str = "VirtualChannel"; + + const FIXED_PART_SIZE: usize = FLAGS_FIELD_SIZE; +} + +impl Encode for VirtualChannel { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.flags.bits()); + + if let Some(value) = self.chunk_size { + dst.write_u32(value); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.chunk_size.map(|_| CHUNK_SIZE_FIELD_SIZE).unwrap_or(0) + } +} + +macro_rules! try_or_return { + ($expr:expr, $ret:expr) => { + match $expr { + Ok(v) => v, + Err(_) => return Ok($ret), + } + }; +} + +impl<'de> Decode<'de> for VirtualChannel { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = VirtualChannelFlags::from_bits_truncate(src.read_u32()); + + let mut virtual_channel_pdu = Self { + flags, + chunk_size: None, + }; + + virtual_channel_pdu.chunk_size = Some(try_or_return!(src.try_read_u32(), virtual_channel_pdu)); + + Ok(virtual_channel_pdu) + } +} diff --git a/ironrdp/src/rdp/capability_sets/virtual_channel/test.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/virtual_channel/tests.rs similarity index 50% rename from ironrdp/src/rdp/capability_sets/virtual_channel/test.rs rename to crates/ironrdp-pdu/src/rdp/capability_sets/virtual_channel/tests.rs index 2e82a276..ff14c89a 100644 --- a/ironrdp/src/rdp/capability_sets/virtual_channel/test.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/virtual_channel/tests.rs @@ -1,4 +1,6 @@ -use lazy_static::lazy_static; +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; use super::*; @@ -11,47 +13,42 @@ const VIRTUAL_CHANNEL_BUFFER: [u8; 8] = [ 0x40, 0x06, 0x00, 0x00, // chunk size ]; -lazy_static! { - pub static ref VIRTUAL_CHANNEL_INCOMPLETE: VirtualChannel = VirtualChannel { - flags: VirtualChannelFlags::COMPRESSION_SERVER_TO_CLIENT, - chunk_size: None, - }; - pub static ref VIRTUAL_CHANNEL: VirtualChannel = VirtualChannel { - flags: VirtualChannelFlags::NO_COMPRESSION, - chunk_size: Some(1600), - }; -} +static VIRTUAL_CHANNEL_INCOMPLETE: LazyLock = LazyLock::new(|| VirtualChannel { + flags: VirtualChannelFlags::COMPRESSION_SERVER_TO_CLIENT, + chunk_size: None, +}); +static VIRTUAL_CHANNEL: LazyLock = LazyLock::new(|| VirtualChannel { + flags: VirtualChannelFlags::NO_COMPRESSION, + chunk_size: Some(1600), +}); #[test] fn from_buffer_correctly_parses_virtual_channel_incomplete_capset() { assert_eq!( *VIRTUAL_CHANNEL_INCOMPLETE, - VirtualChannel::from_buffer(VIRTUAL_CHANNEL_INCOMPLETE_BUFFER.as_ref()).unwrap() + decode(VIRTUAL_CHANNEL_INCOMPLETE_BUFFER.as_ref()).unwrap() ); } #[test] fn from_buffer_correctly_parses_virtual_channel_capset() { - assert_eq!( - *VIRTUAL_CHANNEL, - VirtualChannel::from_buffer(VIRTUAL_CHANNEL_BUFFER.as_ref()).unwrap() - ); + assert_eq!(*VIRTUAL_CHANNEL, decode(VIRTUAL_CHANNEL_BUFFER.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_virtual_channel_incomplete_capset() { - let mut buffer = Vec::new(); + let c = *VIRTUAL_CHANNEL_INCOMPLETE; - VIRTUAL_CHANNEL_INCOMPLETE.to_buffer(&mut buffer).unwrap(); + let buffer = encode_vec(&c).unwrap(); assert_eq!(buffer, VIRTUAL_CHANNEL_INCOMPLETE_BUFFER.as_ref()); } #[test] fn to_buffer_correctly_serializes_virtual_channel_capset() { - let mut buffer = Vec::new(); + let c = *VIRTUAL_CHANNEL; - VIRTUAL_CHANNEL.to_buffer(&mut buffer).unwrap(); + let buffer = encode_vec(&c).unwrap(); assert_eq!(buffer, VIRTUAL_CHANNEL_BUFFER.as_ref()); } @@ -60,14 +57,11 @@ fn to_buffer_correctly_serializes_virtual_channel_capset() { fn buffer_length_is_correct_for_virtual_channel_incomplete_capset() { assert_eq!( VIRTUAL_CHANNEL_INCOMPLETE_BUFFER.len(), - VIRTUAL_CHANNEL_INCOMPLETE.buffer_length() + VIRTUAL_CHANNEL_INCOMPLETE.size() ); } #[test] fn buffer_length_is_correct_for_virtual_channel_capset() { - assert_eq!( - VIRTUAL_CHANNEL_BUFFER.len(), - VIRTUAL_CHANNEL.buffer_length() - ); + assert_eq!(VIRTUAL_CHANNEL_BUFFER.len(), VIRTUAL_CHANNEL.size()); } diff --git a/crates/ironrdp-pdu/src/rdp/client_info.rs b/crates/ironrdp-pdu/src/rdp/client_info.rs new file mode 100644 index 00000000..4180fbec --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/client_info.rs @@ -0,0 +1,883 @@ +use core::fmt; +use std::io; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, write_padding, Decode, DecodeResult, Encode, + EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; +use thiserror::Error; + +use crate::utils::CharacterSet; +use crate::{utils, PduError}; + +const RECONNECT_COOKIE_LEN: usize = 28; +const TIMEZONE_INFO_NAME_LEN: usize = 64; +const COMPRESSION_TYPE_MASK: u32 = 0x0000_1E00; + +const CODE_PAGE_SIZE: usize = 4; +const FLAGS_SIZE: usize = 4; +const DOMAIN_LENGTH_SIZE: usize = 2; +const USER_NAME_LENGTH_SIZE: usize = 2; +const PASSWORD_LENGTH_SIZE: usize = 2; +const ALTERNATE_SHELL_LENGTH_SIZE: usize = 2; +const WORK_DIR_LENGTH_SIZE: usize = 2; + +const CLIENT_ADDRESS_FAMILY_SIZE: usize = 2; +const CLIENT_ADDRESS_LENGTH_SIZE: usize = 2; +const CLIENT_DIR_LENGTH_SIZE: usize = 2; +const SESSION_ID_SIZE: usize = 4; +const PERFORMANCE_FLAGS_SIZE: usize = 4; +const RECONNECT_COOKIE_LENGTH_SIZE: usize = 2; +const BIAS_SIZE: usize = 4; + +/// [2.2.1.11.1.1] Info Packet (TS_INFO_PACKET) +/// +/// [2.2.1.11.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/732394f5-e2b5-4ac5-8a0a-35345386b0d1 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientInfo { + pub credentials: Credentials, + pub code_page: u32, + pub flags: ClientInfoFlags, + pub compression_type: CompressionType, + pub alternate_shell: String, + pub work_dir: String, + pub extra_info: ExtendedClientInfo, +} + +impl ClientInfo { + const NAME: &'static str = "ClientInfo"; + + pub const FIXED_PART_SIZE: usize = CODE_PAGE_SIZE + + FLAGS_SIZE + + DOMAIN_LENGTH_SIZE + + USER_NAME_LENGTH_SIZE + + PASSWORD_LENGTH_SIZE + + ALTERNATE_SHELL_LENGTH_SIZE + + WORK_DIR_LENGTH_SIZE; +} + +impl Encode for ClientInfo { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let character_set = if self.flags.contains(ClientInfoFlags::UNICODE) { + CharacterSet::Unicode + } else { + CharacterSet::Ansi + }; + + dst.write_u32(self.code_page); + + let flags_with_compression_type = self.flags.bits() | (u32::from(self.compression_type.as_u8()) << 9); + dst.write_u32(flags_with_compression_type); + + let domain = self.credentials.domain.clone().unwrap_or_default(); + dst.write_u16(cast_length!( + "domain length", + string_len(domain.as_str(), character_set) + )?); + dst.write_u16(cast_length!( + "username length", + string_len(self.credentials.username.as_str(), character_set) + )?); + dst.write_u16(cast_length!( + "password length", + string_len(self.credentials.password.as_str(), character_set) + )?); + dst.write_u16(cast_length!( + "alternate shell length", + string_len(self.alternate_shell.as_str(), character_set) + )?); + dst.write_u16(cast_length!( + "work dir length", + string_len(self.work_dir.as_str(), character_set) + )?); + + utils::write_string_to_cursor(dst, domain.as_str(), character_set, true)?; + utils::write_string_to_cursor(dst, self.credentials.username.as_str(), character_set, true)?; + utils::write_string_to_cursor(dst, self.credentials.password.as_str(), character_set, true)?; + utils::write_string_to_cursor(dst, self.alternate_shell.as_str(), character_set, true)?; + utils::write_string_to_cursor(dst, self.work_dir.as_str(), character_set, true)?; + + self.extra_info.encode(dst, character_set)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let character_set = if self.flags.contains(ClientInfoFlags::UNICODE) { + CharacterSet::Unicode + } else { + CharacterSet::Ansi + }; + let domain = self.credentials.domain.clone().unwrap_or_default(); + + CODE_PAGE_SIZE + + FLAGS_SIZE + + DOMAIN_LENGTH_SIZE + + USER_NAME_LENGTH_SIZE + + PASSWORD_LENGTH_SIZE + + ALTERNATE_SHELL_LENGTH_SIZE + + WORK_DIR_LENGTH_SIZE + + string_len(domain.as_str(), character_set) + + string_len(self.credentials.username.as_str(), character_set) + + string_len(self.credentials.password.as_str(), character_set) + + string_len(self.alternate_shell.as_str(), character_set) + + string_len(self.work_dir.as_str(), character_set) + + usize::from(character_set.as_u16()) * 5 // null terminator + + self.extra_info.size(character_set) + } +} + +impl<'de> Decode<'de> for ClientInfo { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let code_page = src.read_u32(); + let flags_with_compression_type = src.read_u32(); + + let flags = ClientInfoFlags::from_bits(flags_with_compression_type & !COMPRESSION_TYPE_MASK) + .ok_or_else(|| invalid_field_err!("flags", "invalid ClientInfoFlags"))?; + let compression_type = CompressionType::from_u32((flags_with_compression_type & COMPRESSION_TYPE_MASK) >> 9) + .ok_or_else(|| invalid_field_err!("flags", "invalid CompressionType"))?; + + let character_set = if flags.contains(ClientInfoFlags::UNICODE) { + CharacterSet::Unicode + } else { + CharacterSet::Ansi + }; + + // Sizes exclude the length of the mandatory null terminator + let nt = usize::from(character_set.as_u16()); + let domain_size = usize::from(src.read_u16()) + nt; + let user_name_size = usize::from(src.read_u16()) + nt; + let password_size = usize::from(src.read_u16()) + nt; + let alternate_shell_size = usize::from(src.read_u16()) + nt; + let work_dir_size = usize::from(src.read_u16()) + nt; + ensure_size!(in: src, size: domain_size + user_name_size + password_size + alternate_shell_size + work_dir_size); + + let domain = utils::decode_string(src.read_slice(domain_size), character_set, true)?; + let username = utils::decode_string(src.read_slice(user_name_size), character_set, true)?; + let password = utils::decode_string(src.read_slice(password_size), character_set, true)?; + + let domain = if domain.is_empty() { None } else { Some(domain) }; + let credentials = Credentials { + username, + password, + domain, + }; + + let alternate_shell = utils::decode_string(src.read_slice(alternate_shell_size), character_set, true)?; + let work_dir = utils::decode_string(src.read_slice(work_dir_size), character_set, true)?; + + let extra_info = ExtendedClientInfo::decode(src, character_set)?; + + Ok(Self { + credentials, + code_page, + flags, + compression_type, + alternate_shell, + work_dir, + extra_info, + }) + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct Credentials { + pub username: String, + pub password: String, + pub domain: Option, +} + +impl fmt::Debug for Credentials { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // NOTE: do not show secret (user password) + f.debug_struct("Credentials") + .field("username", &self.username) + .field("domain", &self.domain) + .finish_non_exhaustive() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtendedClientInfo { + pub address_family: AddressFamily, + pub address: String, + pub dir: String, + pub optional_data: ExtendedClientOptionalInfo, +} + +impl ExtendedClientInfo { + // const NAME: &'static str = "ExtendedClientInfo"; + + fn decode(src: &mut ReadCursor<'_>, character_set: CharacterSet) -> DecodeResult { + ensure_size!(in: src, size: CLIENT_ADDRESS_FAMILY_SIZE + CLIENT_ADDRESS_LENGTH_SIZE); + + let address_family = AddressFamily::from_u16(src.read_u16()); + + // This size includes the length of the mandatory null terminator. + let address_size = usize::from(src.read_u16()); + ensure_size!(in: src, size: address_size + CLIENT_DIR_LENGTH_SIZE); + + let address = utils::decode_string(src.read_slice(address_size), character_set, false)?; + // This size includes the length of the mandatory null terminator. + let dir_size = usize::from(src.read_u16()); + ensure_size!(in: src, size: dir_size); + + let dir = utils::decode_string(src.read_slice(dir_size), character_set, false)?; + + let optional_data = ExtendedClientOptionalInfo::decode(src)?; + + Ok(Self { + address_family, + address, + dir, + optional_data, + }) + } + + fn encode(&self, dst: &mut WriteCursor<'_>, character_set: CharacterSet) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size(character_set)); + + let address_string_len: u16 = cast_length!("address length", string_len(self.address.as_str(), character_set))?; + let dir_string_len: u16 = cast_length!("dir length", string_len(self.dir.as_str(), character_set))?; + + dst.write_u16(self.address_family.as_u16()); + // // + size of null terminator, which will write in the write_string function + dst.write_u16(address_string_len + character_set.as_u16()); + utils::write_string_to_cursor(dst, self.address.as_str(), character_set, true)?; + dst.write_u16(dir_string_len + character_set.as_u16()); + utils::write_string_to_cursor(dst, self.dir.as_str(), character_set, true)?; + self.optional_data.encode(dst)?; + + Ok(()) + } + + fn size(&self, character_set: CharacterSet) -> usize { + CLIENT_ADDRESS_FAMILY_SIZE + + CLIENT_ADDRESS_LENGTH_SIZE + + string_len(self.address.as_str(), character_set) + + usize::from(character_set.as_u16()) // null terminator + + CLIENT_DIR_LENGTH_SIZE + + string_len(self.dir.as_str(), character_set) + + usize::from(character_set.as_u16()) // null terminator + + self.optional_data.size() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ExtendedClientOptionalInfo { + timezone: Option, + session_id: Option, + performance_flags: Option, + reconnect_cookie: Option<[u8; RECONNECT_COOKIE_LEN]>, + // other fields are read by RdpVersion::Ten+ +} + +impl ExtendedClientOptionalInfo { + const NAME: &'static str = "ExtendedClientOptionalInfo"; + + /// Creates a new builder for [`ExtendedClientOptionalInfo`]. + pub fn builder( + ) -> builder::ExtendedClientOptionalInfoBuilder { + builder::ExtendedClientOptionalInfoBuilder::::default() + } + + pub fn timezone(&self) -> Option<&TimezoneInfo> { + self.timezone.as_ref() + } + + pub fn session_id(&self) -> Option { + self.session_id + } + + pub fn performance_flags(&self) -> Option { + self.performance_flags + } + + pub fn reconnect_cookie(&self) -> Option<&[u8; RECONNECT_COOKIE_LEN]> { + self.reconnect_cookie.as_ref() + } +} + +impl Encode for ExtendedClientOptionalInfo { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + if let Some(ref timezone) = self.timezone { + timezone.encode(dst)?; + } + if let Some(session_id) = self.session_id { + dst.write_u32(session_id); + } + if let Some(performance_flags) = self.performance_flags { + dst.write_u32(performance_flags.bits()); + } + if let Some(reconnect_cookie) = self.reconnect_cookie { + dst.write_u16(u16::try_from(RECONNECT_COOKIE_LEN).expect("RECONNECT_COOKIE_LEN fit into u16")); + dst.write_array(reconnect_cookie); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let mut size = 0; + + if let Some(ref timezone) = self.timezone { + size += timezone.size(); + } + if self.session_id.is_some() { + size += SESSION_ID_SIZE; + } + if self.performance_flags.is_some() { + size += PERFORMANCE_FLAGS_SIZE; + } + if self.reconnect_cookie.is_some() { + size += RECONNECT_COOKIE_LENGTH_SIZE + RECONNECT_COOKIE_LEN; + } + + size + } +} + +impl<'de> Decode<'de> for ExtendedClientOptionalInfo { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let mut optional_data = Self::default(); + + if src.len() < TimezoneInfo::FIXED_PART_SIZE { + return Ok(optional_data); + } + optional_data.timezone = Some(TimezoneInfo::decode(src)?); + + if src.len() < 4 { + return Ok(optional_data); + } + optional_data.session_id = Some(src.read_u32()); + + if src.len() < 4 { + return Ok(optional_data); + } + optional_data.performance_flags = Some( + PerformanceFlags::from_bits(src.read_u32()) + .ok_or_else(|| invalid_field_err!("performanceFlags", "invalid performance flags"))?, + ); + + if src.len() < 2 { + return Ok(optional_data); + } + let reconnect_cookie_size = src.read_u16(); + if reconnect_cookie_size != u16::try_from(RECONNECT_COOKIE_LEN).expect("RECONNECT_COOKIE_LEN fit into u16") + && reconnect_cookie_size != 0 + { + return Err(invalid_field_err!("cbAutoReconnectCookie", "invalid cookie size")); + } + if reconnect_cookie_size != 0 { + if src.len() < RECONNECT_COOKIE_LEN { + return Err(invalid_field_err!("cbAutoReconnectCookie", "missing cookie data")); + } + optional_data.reconnect_cookie = Some(src.read_array()); + } + + if src.len() < 2 * 2 { + return Ok(optional_data); + } + src.read_u16(); // reserved1 + src.read_u16(); // reserved2 + + Ok(optional_data) + } +} + +/// [2.2.1.11.1.1.1.1] Time Zone Information (TS_TIME_ZONE_INFORMATION) +/// +/// The timezone info struct contains client time zone information. +/// +/// [2.2.1.11.1.1.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/526ed635-d7a9-4d3c-bbe1-4e3fb17585f4 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TimezoneInfo { + pub bias: i32, + pub standard_name: String, + pub standard_date: OptionalSystemTime, + pub standard_bias: i32, + pub daylight_name: String, + pub daylight_date: OptionalSystemTime, + pub daylight_bias: i32, +} + +impl TimezoneInfo { + const NAME: &'static str = "TimezoneInfo"; + + const FIXED_PART_SIZE: usize = BIAS_SIZE + + TIMEZONE_INFO_NAME_LEN + + SystemTime::FIXED_PART_SIZE + + BIAS_SIZE + + TIMEZONE_INFO_NAME_LEN + + SystemTime::FIXED_PART_SIZE + + BIAS_SIZE; +} + +impl Encode for TimezoneInfo { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_i32(self.bias); + + let mut standard_name = utils::to_utf16_bytes(self.standard_name.as_str()); + standard_name.resize(TIMEZONE_INFO_NAME_LEN, 0); + dst.write_slice(&standard_name); + + self.standard_date.encode(dst)?; + dst.write_i32(self.standard_bias); + + let mut daylight_name = utils::to_utf16_bytes(self.daylight_name.as_str()); + daylight_name.resize(TIMEZONE_INFO_NAME_LEN, 0); + dst.write_slice(&daylight_name); + + self.daylight_date.encode(dst)?; + dst.write_i32(self.daylight_bias); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for TimezoneInfo { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let bias = src.read_i32(); + let standard_name = utils::decode_string(src.read_slice(TIMEZONE_INFO_NAME_LEN), CharacterSet::Unicode, false)?; + let standard_date = OptionalSystemTime::decode(src)?; + let standard_bias = src.read_i32(); + + let daylight_name = utils::decode_string(src.read_slice(TIMEZONE_INFO_NAME_LEN), CharacterSet::Unicode, false)?; + let daylight_date = OptionalSystemTime::decode(src)?; + let daylight_bias = src.read_i32(); + + Ok(Self { + bias, + standard_name, + standard_date, + standard_bias, + daylight_name, + daylight_date, + daylight_bias, + }) + } +} + +impl Default for TimezoneInfo { + fn default() -> Self { + Self { + bias: 0, + standard_name: String::new(), + standard_date: OptionalSystemTime(None), + standard_bias: 0, + daylight_name: String::new(), + daylight_date: OptionalSystemTime(None), + daylight_bias: 0, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SystemTime { + pub month: Month, + pub day_of_week: DayOfWeek, + pub day: DayOfWeekOccurrence, + pub hour: u16, + pub minute: u16, + pub second: u16, + pub milliseconds: u16, +} + +impl SystemTime { + const NAME: &'static str = "SystemTime"; + + const FIXED_PART_SIZE: usize = 2 /* Year */ + 2 /* Month */ + 2 /* DoW */ + 2 /* Day */ + 2 /* Hour */ + 2 /* Minute */ + 2 /* Second */ + 2 /* Ms */; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OptionalSystemTime(pub Option); + +impl Encode for OptionalSystemTime { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(0); // year + if let Some(st) = &self.0 { + dst.write_u16(st.month.as_u16()); + dst.write_u16(st.day_of_week.as_u16()); + dst.write_u16(st.day.as_u16()); + dst.write_u16(st.hour); + dst.write_u16(st.minute); + dst.write_u16(st.second); + dst.write_u16(st.milliseconds); + } else { + write_padding!(dst, 2 * 7); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + SystemTime::NAME + } + + fn size(&self) -> usize { + SystemTime::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for OptionalSystemTime { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_size!(in: src, size: SystemTime::FIXED_PART_SIZE); + + let _year = src.read_u16(); // This field MUST be set to zero. + let month = src.read_u16(); + let day_of_week = src.read_u16(); + let day = src.read_u16(); + let hour = src.read_u16(); + let minute = src.read_u16(); + let second = src.read_u16(); + let milliseconds = src.read_u16(); + + match ( + Month::from_u16(month), + DayOfWeek::from_u16(day_of_week), + DayOfWeekOccurrence::from_u16(day), + ) { + (Some(month), Some(day_of_week), Some(day)) => Ok(Self(Some(SystemTime { + month, + day_of_week, + day, + hour, + minute, + second, + milliseconds, + }))), + _ => Ok(Self(None)), + } + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum Month { + January = 1, + February = 2, + March = 3, + April = 4, + May = 5, + June = 6, + July = 7, + August = 8, + September = 9, + October = 10, + November = 11, + December = 12, +} + +impl Month { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum DayOfWeek { + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6, +} + +impl DayOfWeek { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum DayOfWeekOccurrence { + First = 1, + Second = 2, + Third = 3, + Fourth = 4, + Last = 5, +} + +impl DayOfWeekOccurrence { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct PerformanceFlags: u32 { + const DISABLE_WALLPAPER = 0x0000_0001; + const DISABLE_FULLWINDOWDRAG = 0x0000_0002; + const DISABLE_MENUANIMATIONS = 0x0000_0004; + const DISABLE_THEMING = 0x0000_0008; + const RESERVED1 = 0x0000_0010; + const DISABLE_CURSOR_SHADOW = 0x0000_0020; + const DISABLE_CURSORSETTINGS = 0x0000_0040; + const ENABLE_FONT_SMOOTHING = 0x0000_0080; + const ENABLE_DESKTOP_COMPOSITION = 0x0000_0100; + const RESERVED2 = 0x8000_0000; + } +} + +impl Default for PerformanceFlags { + fn default() -> Self { + Self::DISABLE_FULLWINDOWDRAG | Self::DISABLE_MENUANIMATIONS | Self::ENABLE_FONT_SMOOTHING + } +} + +#[repr(transparent)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct AddressFamily(u16); + +impl AddressFamily { + pub const INET: Self = Self(0x0002); + pub const INET_6: Self = Self(0x0017); + + pub fn from_u16(val: u16) -> Self { + Self(val) + } + + pub fn as_u16(self) -> u16 { + self.0 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ClientInfoFlags: u32 { + /// INFO_MOUSE + const MOUSE = 0x0000_0001; + /// INFO_DISABLECTRLALTDEL + const DISABLE_CTRL_ALT_DEL = 0x0000_0002; + /// INFO_AUTOLOGON + const AUTOLOGON = 0x0000_0008; + /// INFO_UNICODE + const UNICODE = 0x0000_0010; + /// INFO_MAXIMIZESHELL + const MAXIMIZE_SHELL = 0x0000_0020; + /// INFO_LOGONNOTIFY + const LOGON_NOTIFY = 0x0000_0040; + /// INFO_COMPRESSION + const COMPRESSION = 0x0000_0080; + /// INFO_ENABLEWINDOWSKEY + const ENABLE_WINDOWS_KEY = 0x0000_0100; + /// INFO_REMOTECONSOLEAUDIO + const REMOTE_CONSOLE_AUDIO = 0x0000_2000; + /// INFO_FORCE_ENCRYPTED_CS_PDU + const FORCE_ENCRYPTED_CS_PDU = 0x0000_4000; + /// INFO_RAIL + const RAIL = 0x0000_8000; + /// INFO_LOGONERRORS + const LOGON_ERRORS = 0x0001_0000; + /// INFO_MOUSE_HAS_WHEEL + const MOUSE_HAS_WHEEL = 0x0002_0000; + /// INFO_PASSWORD_IS_SC_PIN + const PASSWORD_IS_SC_PIN = 0x0004_0000; + /// INFO_NOAUDIOPLAYBACK + const NO_AUDIO_PLAYBACK = 0x0008_0000; + /// INFO_USING_SAVED_CREDS + const USING_SAVED_CREDS = 0x0010_0000; + /// INFO_AUDIOCAPTURE + const AUDIO_CAPTURE = 0x0020_0000; + /// INFO_VIDEO_DISABLE + const VIDEO_DISABLE = 0x0040_0000; + /// INFO_RESERVED1 + const RESERVED1 = 0x0080_0000; + /// INFO_RESERVED1 + const RESERVED2 = 0x0100_0000; + /// INFO_HIDEF_RAIL_SUPPORTED + const HIDEF_RAIL_SUPPORTED = 0x0200_0000; + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum CompressionType { + K8 = 0, + K64 = 1, + Rdp6 = 2, + Rdp61 = 3, +} + +impl CompressionType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[derive(Debug, Error)] +pub enum ClientInfoError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("UTF-8 error")] + Utf8Error(#[from] std::string::FromUtf8Error), + #[error("invalid address family field")] + InvalidAddressFamily, + #[error("invalid flags field")] + InvalidClientInfoFlags, + #[error("invalid performance flags field")] + InvalidPerformanceFlags, + #[error("invalid reconnect cookie field")] + InvalidReconnectCookie, + #[error("PDU error: {0}")] + Pdu(PduError), +} + +impl From for ClientInfoError { + fn from(e: PduError) -> Self { + Self::Pdu(e) + } +} + +fn string_len(value: &str, character_set: CharacterSet) -> usize { + match character_set { + CharacterSet::Ansi => value.len(), + // TODO: Use UTF-16 helper. + CharacterSet::Unicode => value.encode_utf16().count() * 2, + } +} + +pub mod builder { + use core::marker::PhantomData; + + use super::{ExtendedClientOptionalInfo, PerformanceFlags, TimezoneInfo, RECONNECT_COOKIE_LEN}; + + pub struct ExtendedClientOptionalInfoBuilderStateSetTimeZone; + pub struct ExtendedClientOptionalInfoBuilderStateSetSessionId; + pub struct ExtendedClientOptionalInfoBuilderStateSetPerformanceFlags; + pub struct ExtendedClientOptionalInfoBuilderStateSetReconnectCookie; + pub struct ExtendedClientOptionalInfoBuilderStateFinal; + + // State machine-based builder for [`ExtendedClientOptionalInfo`]. + // + // [`ExtendedClientOptionalInfo`] strictly requires to set all preceding optional fields before + // setting the next one, therefore we use a state machine to enforce this during the compile time. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ExtendedClientOptionalInfoBuilder { + inner: ExtendedClientOptionalInfo, + _phantom_data: PhantomData, + } + + impl ExtendedClientOptionalInfoBuilder { + pub fn build(self) -> ExtendedClientOptionalInfo { + self.inner + } + } + + impl ExtendedClientOptionalInfoBuilder { + pub fn new() -> Self { + Self { + inner: ExtendedClientOptionalInfo::default(), + _phantom_data: Default::default(), + } + } + + pub fn timezone( + mut self, + timezone: TimezoneInfo, + ) -> ExtendedClientOptionalInfoBuilder { + self.inner.timezone = Some(timezone); + ExtendedClientOptionalInfoBuilder { + inner: self.inner, + _phantom_data: Default::default(), + } + } + } + + impl Default for ExtendedClientOptionalInfoBuilder { + fn default() -> Self { + Self::new() + } + } + + impl ExtendedClientOptionalInfoBuilder { + pub fn session_id( + mut self, + session_id: u32, + ) -> ExtendedClientOptionalInfoBuilder { + self.inner.session_id = Some(session_id); + ExtendedClientOptionalInfoBuilder { + inner: self.inner, + _phantom_data: Default::default(), + } + } + } + + impl ExtendedClientOptionalInfoBuilder { + pub fn performance_flags( + mut self, + performance_flags: PerformanceFlags, + ) -> ExtendedClientOptionalInfoBuilder { + self.inner.performance_flags = Some(performance_flags); + ExtendedClientOptionalInfoBuilder { + inner: self.inner, + _phantom_data: Default::default(), + } + } + } + + impl ExtendedClientOptionalInfoBuilder { + pub fn reconnect_cookie( + mut self, + reconnect_cookie: [u8; RECONNECT_COOKIE_LEN], + ) -> ExtendedClientOptionalInfoBuilder { + self.inner.reconnect_cookie = Some(reconnect_cookie); + ExtendedClientOptionalInfoBuilder { + inner: self.inner, + _phantom_data: Default::default(), + } + } + } +} diff --git a/crates/ironrdp-pdu/src/rdp/finalization_messages.rs b/crates/ironrdp-pdu/src/rdp/finalization_messages.rs new file mode 100644 index 00000000..bea2710d --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/finalization_messages.rs @@ -0,0 +1,260 @@ +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use crate::gcc; + +const SYNCHRONIZE_PDU_SIZE: usize = 2 + 2; +const CONTROL_PDU_SIZE: usize = 2 + 2 + 4; +const FONT_PDU_SIZE: usize = 2 * 4; +const SYNCHRONIZE_MESSAGE_TYPE: u16 = 1; +const MAX_MONITOR_COUNT: u32 = 64; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SynchronizePdu { + pub target_user_id: u16, +} + +impl SynchronizePdu { + const NAME: &'static str = "SynchronizePdu"; + + const FIXED_PART_SIZE: usize = SYNCHRONIZE_PDU_SIZE; +} + +impl Encode for SynchronizePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(SYNCHRONIZE_MESSAGE_TYPE); + dst.write_u16(self.target_user_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + SYNCHRONIZE_PDU_SIZE + } +} + +impl<'de> Decode<'de> for SynchronizePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let message_type = src.read_u16(); + if message_type != SYNCHRONIZE_MESSAGE_TYPE { + return Err(invalid_field_err!("messageType", "invalid message type")); + } + + let target_user_id = src.read_u16(); + + Ok(Self { target_user_id }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ControlPdu { + pub action: ControlAction, + pub grant_id: u16, + pub control_id: u32, +} + +impl ControlPdu { + const NAME: &'static str = "ControlPdu"; + + const FIXED_PART_SIZE: usize = CONTROL_PDU_SIZE; +} + +impl Encode for ControlPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.action.as_u16()); + dst.write_u16(self.grant_id); + dst.write_u32(self.control_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ControlPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let action = ControlAction::from_u16(src.read_u16()) + .ok_or_else(|| invalid_field_err!("action", "invalid control action"))?; + let grant_id = src.read_u16(); + let control_id = src.read_u32(); + + Ok(Self { + action, + grant_id, + control_id, + }) + } +} + +/// [2.2.1.22.1] Font Map PDU Data (TS_FONT_MAP_PDU) +/// +/// [2.2.1.22.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/b4e557f3-7540-46fc-815d-0c12299cf1ee +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FontPdu { + pub number: u16, + pub total_number: u16, + pub flags: SequenceFlags, + pub entry_size: u16, +} + +impl Default for FontPdu { + fn default() -> Self { + // Those values are recommended in [2.2.1.22.1]. + Self { + number: 0, + total_number: 0, + flags: SequenceFlags::FIRST | SequenceFlags::LAST, + entry_size: 4, + } + } +} + +impl FontPdu { + const NAME: &'static str = "FontPdu"; + + const FIXED_PART_SIZE: usize = FONT_PDU_SIZE; +} + +impl Encode for FontPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.number); + dst.write_u16(self.total_number); + dst.write_u16(self.flags.bits()); + dst.write_u16(self.entry_size); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for FontPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let number = src.read_u16(); + let total_number = src.read_u16(); + let flags = SequenceFlags::from_bits(src.read_u16()) + .ok_or_else(|| invalid_field_err!("flags", "invalid sequence flags"))?; + let entry_size = src.read_u16(); + + Ok(Self { + number, + total_number, + flags, + entry_size, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonitorLayoutPdu { + pub monitors: Vec, +} + +impl MonitorLayoutPdu { + const NAME: &'static str = "MonitorLayoutPdu"; + + const FIXED_PART_SIZE: usize = 4 /* nMonitors */; +} + +impl Encode for MonitorLayoutPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(cast_length!("nMonitors", self.monitors.len())?); + + for monitor in self.monitors.iter() { + monitor.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.monitors.len() * gcc::MONITOR_SIZE + } +} + +impl<'de> Decode<'de> for MonitorLayoutPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let monitor_count = src.read_u32(); + if monitor_count > MAX_MONITOR_COUNT { + return Err(invalid_field_err!("nMonitors", "invalid monitor count")); + } + + let mut monitors = Vec::with_capacity( + usize::try_from(monitor_count) + .expect("monitor_count is guaranteed to fit into usize due to the prior check"), + ); + for _ in 0..monitor_count { + monitors.push(gcc::Monitor::decode(src)?); + } + + Ok(Self { monitors }) + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum ControlAction { + RequestControl = 1, + GrantedControl = 2, + Detach = 3, + Cooperate = 4, +} + +impl ControlAction { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct SequenceFlags: u16 { + const FIRST = 1; + const LAST = 2; + } +} diff --git a/crates/ironrdp-pdu/src/rdp/headers.rs b/crates/ironrdp-pdu/src/rdp/headers.rs new file mode 100644 index 00000000..e97c8add --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/headers.rs @@ -0,0 +1,643 @@ +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, not_enough_bytes_err, other_err, read_padding, + write_padding, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use crate::codecs::rfx::FrameAcknowledgePdu; +use crate::input::InputEventPdu; +use crate::rdp::capability_sets::{ClientConfirmActive, ServerDemandActive}; +use crate::rdp::client_info; +use crate::rdp::finalization_messages::{ControlPdu, FontPdu, MonitorLayoutPdu, SynchronizePdu}; +use crate::rdp::refresh_rectangle::RefreshRectanglePdu; +use crate::rdp::server_error_info::ServerSetErrorInfoPdu; +use crate::rdp::session_info::SaveSessionInfoPdu; +use crate::rdp::suppress_output::SuppressOutputPdu; + +pub const BASIC_SECURITY_HEADER_SIZE: usize = 4; +pub const SHARE_DATA_HEADER_COMPRESSION_MASK: u8 = 0xF; +const SHARE_CONTROL_HEADER_MASK: u16 = 0xF; +const SHARE_CONTROL_HEADER_SIZE: usize = 2 * 3 + 4; + +const PROTOCOL_VERSION: u16 = 0x10; + +// ShareDataHeader +const PADDING_FIELD_SIZE: usize = 1; +const STREAM_ID_FIELD_SIZE: usize = 1; +const UNCOMPRESSED_LENGTH_FIELD_SIZE: usize = 2; +const PDU_TYPE_FIELD_SIZE: usize = 1; +const COMPRESSION_TYPE_FIELD_SIZE: usize = 1; +const COMPRESSED_LENGTH_FIELD_SIZE: usize = 2; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BasicSecurityHeader { + pub flags: BasicSecurityHeaderFlags, +} + +impl BasicSecurityHeader { + const NAME: &'static str = "BasicSecurityHeader"; + + pub const FIXED_PART_SIZE: usize = BASIC_SECURITY_HEADER_SIZE; +} + +impl Encode for BasicSecurityHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.flags.bits()); + dst.write_u16(0); // flags_hi + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for BasicSecurityHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = BasicSecurityHeaderFlags::from_bits(src.read_u16()) + .ok_or_else(|| invalid_field_err!("securityHeader", "invalid basic security header"))?; + let _flags_hi = src.read_u16(); // unused + + Ok(Self { flags }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ShareControlHeader { + pub share_control_pdu: ShareControlPdu, + pub pdu_source: u16, + pub share_id: u32, +} + +impl ShareControlHeader { + const NAME: &'static str = "ShareControlHeader"; + + const FIXED_PART_SIZE: usize = SHARE_CONTROL_HEADER_SIZE; +} + +impl Encode for ShareControlHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let pdu_type_with_version = PROTOCOL_VERSION | self.share_control_pdu.share_header_type().as_u16(); + + dst.write_u16(cast_length!( + "len", + self.share_control_pdu.size() + SHARE_CONTROL_HEADER_SIZE + )?); + dst.write_u16(pdu_type_with_version); + dst.write_u16(self.pdu_source); + dst.write_u32(self.share_id); + + self.share_control_pdu.encode(dst) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.share_control_pdu.size() + } +} + +impl<'de> Decode<'de> for ShareControlHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let total_length = usize::from(src.read_u16()); + let pdu_type_with_version = src.read_u16(); + let pdu_source = src.read_u16(); + let share_id = src.read_u32(); + + let pdu_type = ShareControlPduType::from_u16(pdu_type_with_version & SHARE_CONTROL_HEADER_MASK) + .ok_or_else(|| invalid_field_err!("pdu_type", "invalid pdu type"))?; + let pdu_version = pdu_type_with_version & !SHARE_CONTROL_HEADER_MASK; + if pdu_version != PROTOCOL_VERSION { + return Err(invalid_field_err!("pdu_version", "invalid PDU version")); + } + + let share_pdu = ShareControlPdu::from_type(src, pdu_type)?; + let header = Self { + share_control_pdu: share_pdu, + pdu_source, + share_id, + }; + + if pdu_type == ShareControlPduType::DataPdu { + // Some windows version have an issue where + // there is some padding not part of the inner unit. + // Consume that data + let header_length = header.size(); + + if header_length != total_length { + if total_length < header_length { + return Err(not_enough_bytes_err!(total_length, header_length)); + } + + let padding = total_length - header_length; + ensure_size!(in: src, size: padding); + read_padding!(src, padding); + } + } + + Ok(header) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ShareControlPdu { + ServerDemandActive(ServerDemandActive), + ClientConfirmActive(ClientConfirmActive), + Data(ShareDataHeader), + ServerDeactivateAll(ServerDeactivateAll), +} + +impl ShareControlPdu { + const NAME: &'static str = "ShareControlPdu"; + + pub fn as_short_name(&self) -> &str { + match self { + ShareControlPdu::ServerDemandActive(_) => "Server Demand Active PDU", + ShareControlPdu::ClientConfirmActive(_) => "Client Confirm Active PDU", + ShareControlPdu::Data(_) => "Data PDU", + ShareControlPdu::ServerDeactivateAll(_) => "Server Deactivate All PDU", + } + } + + pub fn share_header_type(&self) -> ShareControlPduType { + match self { + ShareControlPdu::ServerDemandActive(_) => ShareControlPduType::DemandActivePdu, + ShareControlPdu::ClientConfirmActive(_) => ShareControlPduType::ConfirmActivePdu, + ShareControlPdu::Data(_) => ShareControlPduType::DataPdu, + ShareControlPdu::ServerDeactivateAll(_) => ShareControlPduType::DeactivateAllPdu, + } + } + + pub fn from_type(src: &mut ReadCursor<'_>, share_type: ShareControlPduType) -> DecodeResult { + match share_type { + ShareControlPduType::DemandActivePdu => { + Ok(ShareControlPdu::ServerDemandActive(ServerDemandActive::decode(src)?)) + } + ShareControlPduType::ConfirmActivePdu => { + Ok(ShareControlPdu::ClientConfirmActive(ClientConfirmActive::decode(src)?)) + } + ShareControlPduType::DataPdu => Ok(ShareControlPdu::Data(ShareDataHeader::decode(src)?)), + ShareControlPduType::DeactivateAllPdu => { + Ok(ShareControlPdu::ServerDeactivateAll(ServerDeactivateAll::decode(src)?)) + } + _ => Err(invalid_field_err!("share_type", "unexpected share control PDU type")), + } + } +} + +impl Encode for ShareControlPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + ShareControlPdu::ServerDemandActive(pdu) => pdu.encode(dst), + ShareControlPdu::ClientConfirmActive(pdu) => pdu.encode(dst), + ShareControlPdu::Data(share_data_header) => share_data_header.encode(dst), + ShareControlPdu::ServerDeactivateAll(deactivate_all) => deactivate_all.encode(dst), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + match self { + ShareControlPdu::ServerDemandActive(pdu) => pdu.size(), + ShareControlPdu::ClientConfirmActive(pdu) => pdu.size(), + ShareControlPdu::Data(share_data_header) => share_data_header.size(), + ShareControlPdu::ServerDeactivateAll(deactivate_all) => deactivate_all.size(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ShareDataHeader { + pub share_data_pdu: ShareDataPdu, + pub stream_priority: StreamPriority, + pub compression_flags: CompressionFlags, + pub compression_type: client_info::CompressionType, +} + +impl ShareDataHeader { + const NAME: &'static str = "ShareDataHeader"; + + const FIXED_PART_SIZE: usize = PADDING_FIELD_SIZE + + STREAM_ID_FIELD_SIZE + + UNCOMPRESSED_LENGTH_FIELD_SIZE + + PDU_TYPE_FIELD_SIZE + + COMPRESSION_TYPE_FIELD_SIZE + + COMPRESSED_LENGTH_FIELD_SIZE; +} + +impl Encode for ShareDataHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + if self.compression_flags.is_empty() { + let compression_flags_with_type = self.compression_flags.bits() | self.compression_type.as_u8(); + + write_padding!(dst, 1); + dst.write_u8(self.stream_priority.as_u8()); + dst.write_u16(cast_length!( + "uncompressedLength", + self.share_data_pdu.size() + + PDU_TYPE_FIELD_SIZE + + COMPRESSION_TYPE_FIELD_SIZE + + COMPRESSED_LENGTH_FIELD_SIZE + )?); + dst.write_u8(self.share_data_pdu.share_header_type().as_u8()); + dst.write_u8(compression_flags_with_type); + dst.write_u16(0); // compressed length + + self.share_data_pdu.encode(dst) + } else { + Err(other_err!("Compression is not implemented")) + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.share_data_pdu.size() + } +} + +impl<'de> Decode<'de> for ShareDataHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + read_padding!(src, 1); + let stream_priority = StreamPriority::from_u8(src.read_u8()) + .ok_or_else(|| invalid_field_err!("streamPriority", "Invalid stream priority"))?; + let _uncompressed_length = src.read_u16(); + let pdu_type = ShareDataPduType::from_u8(src.read_u8()) + .ok_or_else(|| invalid_field_err!("pduType", "Invalid pdu type"))?; + let compression_flags_with_type = src.read_u8(); + + let compression_flags = + CompressionFlags::from_bits_truncate(compression_flags_with_type & !SHARE_DATA_HEADER_COMPRESSION_MASK); + let compression_type = + client_info::CompressionType::from_u8(compression_flags_with_type & SHARE_DATA_HEADER_COMPRESSION_MASK) + .ok_or_else(|| invalid_field_err!("compressionType", "Invalid compression type"))?; + let _compressed_length = src.read_u16(); + + let share_data_pdu = ShareDataPdu::from_type(src, pdu_type)?; + + Ok(Self { + share_data_pdu, + stream_priority, + compression_flags, + compression_type, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ShareDataPdu { + Synchronize(SynchronizePdu), + Control(ControlPdu), + FontList(FontPdu), + FontMap(FontPdu), + MonitorLayout(MonitorLayoutPdu), + SaveSessionInfo(SaveSessionInfoPdu), + FrameAcknowledge(FrameAcknowledgePdu), + ServerSetErrorInfo(ServerSetErrorInfoPdu), + Input(InputEventPdu), + ShutdownRequest, + ShutdownDenied, + SuppressOutput(SuppressOutputPdu), + RefreshRectangle(RefreshRectanglePdu), + Update(Vec), + Pointer(Vec), + PlaySound(Vec), + SetKeyboardIndicators(Vec), + BitmapCachePersistentList(Vec), + BitmapCacheErrorPdu(Vec), + SetKeyboardImeStatus(Vec), + OffscreenCacheErrorPdu(Vec), + DrawNineGridErrorPdu(Vec), + DrawGdiPusErrorPdu(Vec), + ArcStatusPdu(Vec), + StatusInfoPdu(Vec), +} + +impl ShareDataPdu { + const NAME: &'static str = "ShareDataPdu"; + + pub fn as_short_name(&self) -> &str { + match self { + ShareDataPdu::Synchronize(_) => "Synchronize PDU", + ShareDataPdu::Control(_) => "Control PDU", + ShareDataPdu::FontList(_) => "FontList PDU", + ShareDataPdu::FontMap(_) => "Font Map PDU", + ShareDataPdu::MonitorLayout(_) => "Monitor Layout PDU", + ShareDataPdu::SaveSessionInfo(_) => "Save session info PDU", + ShareDataPdu::FrameAcknowledge(_) => "Frame Acknowledge PDU", + ShareDataPdu::ServerSetErrorInfo(_) => "Server Set Error Info PDU", + ShareDataPdu::Input(_) => "Server Input PDU", + ShareDataPdu::ShutdownRequest => "Shutdown Request PDU", + ShareDataPdu::ShutdownDenied => "Shutdown Denied PDU", + ShareDataPdu::SuppressOutput(_) => "Suppress Output PDU", + ShareDataPdu::RefreshRectangle(_) => "Refresh Rectangle PDU", + ShareDataPdu::Update(_) => "Update PDU", + ShareDataPdu::Pointer(_) => "Pointer PDU", + ShareDataPdu::PlaySound(_) => "Play Sound PDU", + ShareDataPdu::SetKeyboardIndicators(_) => "Set Keyboard Indicators PDU", + ShareDataPdu::BitmapCachePersistentList(_) => "Bitmap Cache Persistent List PDU", + ShareDataPdu::BitmapCacheErrorPdu(_) => "Bitmap Cache Error PDU", + ShareDataPdu::SetKeyboardImeStatus(_) => "Set Keyboard IME Status PDU", + ShareDataPdu::OffscreenCacheErrorPdu(_) => "Offscreen Cache Error PDU", + ShareDataPdu::DrawNineGridErrorPdu(_) => "Draw Nine Grid Error PDU", + ShareDataPdu::DrawGdiPusErrorPdu(_) => "Draw GDI PUS Error PDU", + ShareDataPdu::ArcStatusPdu(_) => "Arc Status PDU", + ShareDataPdu::StatusInfoPdu(_) => "Status Info PDU", + } + } + + pub fn share_header_type(&self) -> ShareDataPduType { + match self { + ShareDataPdu::Synchronize(_) => ShareDataPduType::Synchronize, + ShareDataPdu::Control(_) => ShareDataPduType::Control, + ShareDataPdu::FontList(_) => ShareDataPduType::FontList, + ShareDataPdu::FontMap(_) => ShareDataPduType::FontMap, + ShareDataPdu::MonitorLayout(_) => ShareDataPduType::MonitorLayoutPdu, + ShareDataPdu::SaveSessionInfo(_) => ShareDataPduType::SaveSessionInfo, + ShareDataPdu::FrameAcknowledge(_) => ShareDataPduType::FrameAcknowledgePdu, + ShareDataPdu::ServerSetErrorInfo(_) => ShareDataPduType::SetErrorInfoPdu, + ShareDataPdu::Input(_) => ShareDataPduType::Input, + ShareDataPdu::ShutdownRequest => ShareDataPduType::ShutdownRequest, + ShareDataPdu::ShutdownDenied => ShareDataPduType::ShutdownDenied, + ShareDataPdu::SuppressOutput(_) => ShareDataPduType::SuppressOutput, + ShareDataPdu::RefreshRectangle(_) => ShareDataPduType::RefreshRectangle, + ShareDataPdu::Update(_) => ShareDataPduType::Update, + ShareDataPdu::Pointer(_) => ShareDataPduType::Pointer, + ShareDataPdu::PlaySound(_) => ShareDataPduType::PlaySound, + ShareDataPdu::SetKeyboardIndicators(_) => ShareDataPduType::SetKeyboardIndicators, + ShareDataPdu::BitmapCachePersistentList(_) => ShareDataPduType::BitmapCachePersistentList, + ShareDataPdu::BitmapCacheErrorPdu(_) => ShareDataPduType::BitmapCacheErrorPdu, + ShareDataPdu::SetKeyboardImeStatus(_) => ShareDataPduType::SetKeyboardImeStatus, + ShareDataPdu::OffscreenCacheErrorPdu(_) => ShareDataPduType::OffscreenCacheErrorPdu, + ShareDataPdu::DrawNineGridErrorPdu(_) => ShareDataPduType::DrawNineGridErrorPdu, + ShareDataPdu::DrawGdiPusErrorPdu(_) => ShareDataPduType::DrawGdiPusErrorPdu, + ShareDataPdu::ArcStatusPdu(_) => ShareDataPduType::ArcStatusPdu, + ShareDataPdu::StatusInfoPdu(_) => ShareDataPduType::StatusInfoPdu, + } + } + + fn from_type(src: &mut ReadCursor<'_>, share_type: ShareDataPduType) -> DecodeResult { + match share_type { + ShareDataPduType::Synchronize => Ok(ShareDataPdu::Synchronize(SynchronizePdu::decode(src)?)), + ShareDataPduType::Control => Ok(ShareDataPdu::Control(ControlPdu::decode(src)?)), + ShareDataPduType::FontList => Ok(ShareDataPdu::FontList(FontPdu::decode(src)?)), + ShareDataPduType::FontMap => Ok(ShareDataPdu::FontMap(FontPdu::decode(src)?)), + ShareDataPduType::MonitorLayoutPdu => Ok(ShareDataPdu::MonitorLayout(MonitorLayoutPdu::decode(src)?)), + ShareDataPduType::SaveSessionInfo => Ok(ShareDataPdu::SaveSessionInfo(SaveSessionInfoPdu::decode(src)?)), + ShareDataPduType::FrameAcknowledgePdu => { + Ok(ShareDataPdu::FrameAcknowledge(FrameAcknowledgePdu::decode(src)?)) + } + ShareDataPduType::SetErrorInfoPdu => { + Ok(ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu::decode(src)?)) + } + ShareDataPduType::Input => Ok(ShareDataPdu::Input(InputEventPdu::decode(src)?)), + ShareDataPduType::ShutdownRequest => Ok(ShareDataPdu::ShutdownRequest), + ShareDataPduType::ShutdownDenied => Ok(ShareDataPdu::ShutdownDenied), + ShareDataPduType::SuppressOutput => Ok(ShareDataPdu::SuppressOutput(SuppressOutputPdu::decode(src)?)), + ShareDataPduType::RefreshRectangle => Ok(ShareDataPdu::RefreshRectangle(RefreshRectanglePdu::decode(src)?)), + ShareDataPduType::Update => Ok(ShareDataPdu::Update(src.remaining().to_vec())), + ShareDataPduType::Pointer => Ok(ShareDataPdu::Pointer(src.remaining().to_vec())), + ShareDataPduType::PlaySound => Ok(ShareDataPdu::PlaySound(src.remaining().to_vec())), + ShareDataPduType::SetKeyboardIndicators => { + Ok(ShareDataPdu::SetKeyboardIndicators(src.remaining().to_vec())) + } + ShareDataPduType::BitmapCachePersistentList => { + Ok(ShareDataPdu::BitmapCachePersistentList(src.remaining().to_vec())) + } + ShareDataPduType::BitmapCacheErrorPdu => Ok(ShareDataPdu::BitmapCacheErrorPdu(src.remaining().to_vec())), + ShareDataPduType::SetKeyboardImeStatus => Ok(ShareDataPdu::SetKeyboardImeStatus(src.remaining().to_vec())), + ShareDataPduType::OffscreenCacheErrorPdu => { + Ok(ShareDataPdu::OffscreenCacheErrorPdu(src.remaining().to_vec())) + } + ShareDataPduType::DrawNineGridErrorPdu => Ok(ShareDataPdu::DrawNineGridErrorPdu(src.remaining().to_vec())), + ShareDataPduType::DrawGdiPusErrorPdu => Ok(ShareDataPdu::DrawGdiPusErrorPdu(src.remaining().to_vec())), + ShareDataPduType::ArcStatusPdu => Ok(ShareDataPdu::ArcStatusPdu(src.remaining().to_vec())), + ShareDataPduType::StatusInfoPdu => Ok(ShareDataPdu::StatusInfoPdu(src.remaining().to_vec())), + } + } +} + +impl Encode for ShareDataPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + ShareDataPdu::Synchronize(pdu) => pdu.encode(dst), + ShareDataPdu::Control(pdu) => pdu.encode(dst), + ShareDataPdu::FontList(pdu) | ShareDataPdu::FontMap(pdu) => pdu.encode(dst), + ShareDataPdu::MonitorLayout(pdu) => pdu.encode(dst), + ShareDataPdu::SaveSessionInfo(pdu) => pdu.encode(dst), + ShareDataPdu::FrameAcknowledge(pdu) => pdu.encode(dst), + ShareDataPdu::ServerSetErrorInfo(pdu) => pdu.encode(dst), + ShareDataPdu::Input(pdu) => pdu.encode(dst), + ShareDataPdu::ShutdownRequest | ShareDataPdu::ShutdownDenied => Ok(()), + ShareDataPdu::SuppressOutput(pdu) => pdu.encode(dst), + ShareDataPdu::RefreshRectangle(pdu) => pdu.encode(dst), + _ => Err(other_err!("Encoding not implemented")), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + match self { + ShareDataPdu::Synchronize(pdu) => pdu.size(), + ShareDataPdu::Control(pdu) => pdu.size(), + ShareDataPdu::FontList(pdu) | ShareDataPdu::FontMap(pdu) => pdu.size(), + ShareDataPdu::MonitorLayout(pdu) => pdu.size(), + ShareDataPdu::SaveSessionInfo(pdu) => pdu.size(), + ShareDataPdu::FrameAcknowledge(pdu) => pdu.size(), + ShareDataPdu::ServerSetErrorInfo(pdu) => pdu.size(), + ShareDataPdu::Input(pdu) => pdu.size(), + ShareDataPdu::ShutdownRequest | ShareDataPdu::ShutdownDenied => 0, + ShareDataPdu::SuppressOutput(pdu) => pdu.size(), + ShareDataPdu::RefreshRectangle(pdu) => pdu.size(), + ShareDataPdu::Update(buffer) + | ShareDataPdu::Pointer(buffer) + | ShareDataPdu::PlaySound(buffer) + | ShareDataPdu::SetKeyboardIndicators(buffer) + | ShareDataPdu::BitmapCachePersistentList(buffer) + | ShareDataPdu::BitmapCacheErrorPdu(buffer) + | ShareDataPdu::SetKeyboardImeStatus(buffer) + | ShareDataPdu::OffscreenCacheErrorPdu(buffer) + | ShareDataPdu::DrawNineGridErrorPdu(buffer) + | ShareDataPdu::DrawGdiPusErrorPdu(buffer) + | ShareDataPdu::ArcStatusPdu(buffer) + | ShareDataPdu::StatusInfoPdu(buffer) => buffer.len(), + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct BasicSecurityHeaderFlags: u16 { + const EXCHANGE_PKT = 0x0001; + const TRANSPORT_REQ = 0x0002; + const TRANSPORT_RSP = 0x0004; + const ENCRYPT = 0x0008; + const RESET_SEQNO = 0x0010; + const IGNORE_SEQNO = 0x0020; + const INFO_PKT = 0x0040; + const LICENSE_PKT = 0x0080; + const LICENSE_ENCRYPT_CS = 0x0100; + const LICENSE_ENCRYPT_SC = 0x0200; + const REDIRECTION_PKT = 0x0400; + const SECURE_CHECKSUM = 0x0800; + const AUTODETECT_REQ = 0x1000; + const AUTODETECT_RSP = 0x2000; + const HEARTBEAT = 0x4000; + const FLAGSHI_VALID = 0x8000; + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum StreamPriority { + Undefined = 0, + Low = 1, + Medium = 2, + High = 4, +} + +impl StreamPriority { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum ShareControlPduType { + DemandActivePdu = 0x1, + ConfirmActivePdu = 0x3, + DeactivateAllPdu = 0x6, + DataPdu = 0x7, + ServerRedirect = 0xa, +} + +impl ShareControlPduType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(u8)] +pub enum ShareDataPduType { + Update = 0x02, + Control = 0x14, + Pointer = 0x1b, + Input = 0x1c, + Synchronize = 0x1f, + RefreshRectangle = 0x21, + PlaySound = 0x22, + SuppressOutput = 0x23, + ShutdownRequest = 0x24, + ShutdownDenied = 0x25, + SaveSessionInfo = 0x26, + FontList = 0x27, + FontMap = 0x28, + SetKeyboardIndicators = 0x29, + BitmapCachePersistentList = 0x2b, + BitmapCacheErrorPdu = 0x2c, + SetKeyboardImeStatus = 0x2d, + OffscreenCacheErrorPdu = 0x2e, + SetErrorInfoPdu = 0x2f, + DrawNineGridErrorPdu = 0x30, + DrawGdiPusErrorPdu = 0x31, + ArcStatusPdu = 0x32, + StatusInfoPdu = 0x36, + MonitorLayoutPdu = 0x37, + FrameAcknowledgePdu = 0x38, +} + +impl ShareDataPduType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CompressionFlags: u8 { + const COMPRESSED = 0x20; + const AT_FRONT = 0x40; + const FLUSHED = 0x80; + } +} + +/// 2.2.3.1 Server Deactivate All PDU +/// +/// [2.2.3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/8a29971a-df3c-48da-add2-8ed9a05edc89 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerDeactivateAll; + +impl ServerDeactivateAll { + const FIXED_PART_SIZE: usize = 2 /* length_source_descriptor */ + 1 /* source_descriptor */; +} + +impl Decode<'_> for ServerDeactivateAll { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let length_source_descriptor = src.read_u16(); + ensure_size!(in: src, size: length_source_descriptor.into()); + let _ = src.read_slice(length_source_descriptor.into()); + Ok(Self) + } +} + +impl Encode for ServerDeactivateAll { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + // A 16-bit, unsigned integer. The size in bytes of the sourceDescriptor field. + dst.write_u16(1); + // Variable number of bytes. The source descriptor. This field SHOULD be set to 0x00. + dst.write_u8(0); + Ok(()) + } + + fn name(&self) -> &'static str { + "Server Deactivate All" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} diff --git a/crates/ironrdp-pdu/src/rdp/mod.rs b/crates/ironrdp-pdu/src/rdp/mod.rs new file mode 100644 index 00000000..1d28a272 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/mod.rs @@ -0,0 +1,117 @@ +use std::io; + +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use thiserror::Error; + +use crate::input::InputEventError; +use crate::rdp::capability_sets::CapabilitySetsError; +use crate::rdp::client_info::{ClientInfo, ClientInfoError}; +use crate::rdp::headers::{BasicSecurityHeader, BasicSecurityHeaderFlags, ShareControlPduType, ShareDataPduType}; +use crate::rdp::server_license::ServerLicenseError; +use crate::PduError; + +pub mod capability_sets; +pub mod client_info; +pub mod finalization_messages; +pub mod headers; +pub mod refresh_rectangle; +pub mod server_error_info; +pub mod server_license; +pub mod session_info; +pub mod suppress_output; +pub mod vc; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientInfoPdu { + pub security_header: BasicSecurityHeader, + pub client_info: ClientInfo, +} + +impl ClientInfoPdu { + const NAME: &'static str = "ClientInfoPDU"; + + const FIXED_PART_SIZE: usize = BasicSecurityHeader::FIXED_PART_SIZE + ClientInfo::FIXED_PART_SIZE; +} + +impl Encode for ClientInfoPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + self.security_header.encode(dst)?; + self.client_info.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.security_header.size() + self.client_info.size() + } +} + +impl<'de> Decode<'de> for ClientInfoPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let security_header = BasicSecurityHeader::decode(src)?; + if !security_header.flags.contains(BasicSecurityHeaderFlags::INFO_PKT) { + return Err(invalid_field_err!("securityHeader", "got invalid security header")); + } + + let client_info = ClientInfo::decode(src)?; + + Ok(Self { + security_header, + client_info, + }) + } +} + +#[derive(Debug, Error)] +pub enum RdpError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("client Info PDU error")] + ClientInfoError(#[from] ClientInfoError), + #[error("server License PDU error")] + ServerLicenseError(#[from] ServerLicenseError), + #[error("capability sets error")] + CapabilitySetsError(#[from] CapabilitySetsError), + #[error("invalid RDP security header")] + InvalidSecurityHeader, + #[error("invalid RDP Share Control Header: {0}")] + InvalidShareControlHeader(String), + #[error("invalid RDP Share Data Header: {0}")] + InvalidShareDataHeader(String), + #[error("invalid RDP Connection Sequence PDU")] + InvalidPdu(String), + #[error("unexpected RDP Share Control Header PDU type: {0:?}")] + UnexpectedShareControlPdu(ShareControlPduType), + #[error("unexpected RDP Share Data Header PDU type: {0:?}")] + UnexpectedShareDataPdu(ShareDataPduType), + #[error("save session info PDU error")] + SaveSessionInfoError(#[from] session_info::SessionError), + #[error("input event PDU error")] + InputEventError(#[from] InputEventError), + #[error("not enough bytes")] + NotEnoughBytes, + #[error("PDU error: {0}")] + Pdu(PduError), +} + +impl From for RdpError { + fn from(e: PduError) -> Self { + Self::Pdu(e) + } +} + +impl From for io::Error { + fn from(e: RdpError) -> io::Error { + io::Error::other(format!("RDP Connection Sequence error: {e}")) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/refresh_rectangle.rs b/crates/ironrdp-pdu/src/rdp/refresh_rectangle.rs new file mode 100644 index 00000000..089f39af --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/refresh_rectangle.rs @@ -0,0 +1,65 @@ +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, read_padding, write_padding, Decode, DecodeResult, Encode, + EncodeResult, ReadCursor, WriteCursor, +}; + +use crate::geometry::InclusiveRectangle; + +/// [2.2.11.2.1] Refresh Rect PDU Data (TS_REFRESH_RECT_PDU) +/// +/// The Refresh Rect PDU allows the client to request that the server redraw one +/// or more rectangles of the session screen area. The client can use it to +/// repaint sections of the client window that were obscured by local +/// applications. Server support for this PDU is indicated in the General +/// Capability Set (section [2.2.7.1.1]. +/// +/// [2.2.11.2.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/fe04a39d-dc10-489f-bea7-08dad5538547 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RefreshRectanglePdu { + pub areas_to_refresh: Vec, +} + +impl RefreshRectanglePdu { + const NAME: &'static str = "RefreshRectanglePdu"; + + const FIXED_PART_SIZE: usize = 1 /* numberOfAreas */ + 3 /* pad3Octets */; +} + +impl Encode for RefreshRectanglePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let n_areas = cast_length!("nAreas", self.areas_to_refresh.len())?; + + dst.write_u8(n_areas); + write_padding!(dst, 3); + for rectangle in self.areas_to_refresh.iter() { + rectangle.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.areas_to_refresh.iter().map(|r| r.size()).sum::() + // areasToRefresh + } +} + +impl<'de> Decode<'de> for RefreshRectanglePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let number_of_areas = usize::from(src.read_u8()); + read_padding!(src, 3); + let areas_to_refresh = core::iter::repeat_with(|| InclusiveRectangle::decode(src)) + .take(number_of_areas) + .collect::, _>>()?; + + Ok(Self { areas_to_refresh }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/server_error_info.rs b/crates/ironrdp-pdu/src/rdp/server_error_info.rs new file mode 100644 index 00000000..28a51521 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_error_info.rs @@ -0,0 +1,456 @@ +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerSetErrorInfoPdu(pub ErrorInfo); + +impl ServerSetErrorInfoPdu { + const NAME: &'static str = "ServerSetErrorInfoPdu"; + + const FIXED_PART_SIZE: usize = 4 /* errorInfo */; +} + +impl Encode for ServerSetErrorInfoPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.0.as_u32()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ServerSetErrorInfoPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let error_info = src.read_u32(); + let error_info = + ErrorInfo::from_u32(error_info).ok_or_else(|| invalid_field_err!("errorInfo", "unexpected info code"))?; + + Ok(Self(error_info)) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ErrorInfo { + ProtocolIndependentCode(ProtocolIndependentCode), + ProtocolIndependentLicensingCode(ProtocolIndependentLicensingCode), + ProtocolIndependentConnectionBrokerCode(ProtocolIndependentConnectionBrokerCode), + RdpSpecificCode(RdpSpecificCode), +} + +impl ErrorInfo { + pub fn description(self) -> String { + match self { + Self::ProtocolIndependentCode(c) => { + format!("[Protocol independent error] {}", c.description()) + } + Self::ProtocolIndependentLicensingCode(c) => { + format!("[Protocol independent licensing error] {}", c.description()) + } + Self::ProtocolIndependentConnectionBrokerCode(c) => { + format!("[Protocol independent connection broker error] {}", c.description()) + } + Self::RdpSpecificCode(c) => format!("[RDP specific code]: {}", c.description()), + } + } + + fn as_u32(self) -> u32 { + match self { + Self::ProtocolIndependentCode(c) => c.as_u32(), + Self::ProtocolIndependentLicensingCode(c) => c.as_u32(), + Self::ProtocolIndependentConnectionBrokerCode(c) => c.as_u32(), + Self::RdpSpecificCode(c) => c.as_u32(), + } + } +} + +impl FromPrimitive for ErrorInfo { + fn from_i64(n: i64) -> Option { + if let Some(v) = ProtocolIndependentCode::from_i64(n) { + Some(Self::ProtocolIndependentCode(v)) + } else if let Some(v) = ProtocolIndependentLicensingCode::from_i64(n) { + Some(Self::ProtocolIndependentLicensingCode(v)) + } else if let Some(v) = ProtocolIndependentConnectionBrokerCode::from_i64(n) { + Some(Self::ProtocolIndependentConnectionBrokerCode(v)) + } else { + RdpSpecificCode::from_i64(n).map(Self::RdpSpecificCode) + } + } + + fn from_u64(n: u64) -> Option { + if let Some(v) = ProtocolIndependentCode::from_u64(n) { + Some(Self::ProtocolIndependentCode(v)) + } else if let Some(v) = ProtocolIndependentLicensingCode::from_u64(n) { + Some(Self::ProtocolIndependentLicensingCode(v)) + } else if let Some(v) = ProtocolIndependentConnectionBrokerCode::from_u64(n) { + Some(Self::ProtocolIndependentConnectionBrokerCode(v)) + } else { + RdpSpecificCode::from_u64(n).map(Self::RdpSpecificCode) + } + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum ProtocolIndependentCode { + None = 0x0000_0000, + RpcInitiatedDisconnect = 0x0000_0001, + RpcInitiatedLogoff = 0x0000_0002, + IdleTimeout = 0x0000_0003, + LogonTimeout = 0x0000_0004, + DisconnectedByOtherconnection = 0x0000_0005, + OutOfMemory = 0x0000_0006, + ServerDeniedConnection = 0x0000_0007, + ServerInsufficientPrivileges = 0x0000_0009, + ServerFreshCredentialsRequired = 0x0000_000A, + RpcInitiatedDisconnectByuser = 0x0000_000B, + LogoffByUser = 0x0000_000C, + CloseStackOnDriverNotReady = 0x0000_000F, + ServerDwmCrash = 0x0000_0010, + CloseStackOnDriverFailure = 0x0000_0011, + CloseStackOnDriverIfaceFailure = 0x0000_0012, + ServerWinlogonCrash = 0x0000_0017, + ServerCsrssCrash = 0x0000_0018, +} + +impl ProtocolIndependentCode { + pub fn description(&self) -> &str { + match self { + Self::None => "No error has occurred", + Self::RpcInitiatedDisconnect => "The disconnection was initiated by an administrative tool on the server in another session", + Self::RpcInitiatedLogoff => "The disconnection was due to a forced logoff initiated by an administrative tool on the server in another session", + Self::IdleTimeout => "The idle session limit timer on the server has elapsed", + Self::LogonTimeout => "The active session limit timer on the server has elapsed", + Self::DisconnectedByOtherconnection => "Another user connected to the server, forcing the disconnection of the current connection", + Self::OutOfMemory => "The server ran out of available memory resources", + Self::ServerDeniedConnection => "The server denied the connection", + Self::ServerInsufficientPrivileges => "The user cannot connect to the server due to insufficient access privileges", + Self::ServerFreshCredentialsRequired => "The server does not accept saved user credentials and requires that the user enter their credentials for each connection", + Self::RpcInitiatedDisconnectByuser => "The disconnection was initiated by an administrative tool on the server running in the user's session", + Self::LogoffByUser => "The disconnection was initiated by the user logging off his or her session on the server", + Self::CloseStackOnDriverNotReady => "The display driver in the remote session did not report any status within the time allotted for startup", + Self::ServerDwmCrash => "The DWM process running in the remote session terminated unexpectedly", + Self::CloseStackOnDriverFailure => "The display driver in the remote session was unable to complete all the tasks required for startup", + Self::CloseStackOnDriverIfaceFailure => "The display driver in the remote session started up successfully, but due to internal failures was not usable by the remoting stack", + Self::ServerWinlogonCrash => "The Winlogon process running in the remote session terminated unexpectedly", + Self::ServerCsrssCrash => "The CSRSS process running in the remote session terminated unexpectedly", + } + } + + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u32(self) -> u32 { + self as u32 + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum ProtocolIndependentLicensingCode { + Internal = 0x0000_0100, + NoLicenseServer = 0x0000_0101, + NoLicense = 0x0000_0102, + BadClientMsg = 0x0000_0103, + HwidDoesntMatchLicense = 0x0000_0104, + BadClientLicense = 0x0000_0105, + CantFinishProtocol = 0x0000_0106, + ClientEndedProtocol = 0x0000_0107, + BadClientEncryption = 0x0000_0108, + CantUpgradeLicense = 0x0000_0109, + NoRemoteConnections = 0x0000_010A, +} + +impl ProtocolIndependentLicensingCode { + pub fn description(&self) -> &str { + match self { + Self::Internal => "An internal error has occurred in the Terminal Services licensing component", + Self::NoLicenseServer => "A Remote Desktop License Server could not be found to provide a license", + Self::NoLicense => "There are no Client Access Licenses available for the target remote computer", + Self::BadClientMsg => "The remote computer received an invalid licensing message from the client", + Self::HwidDoesntMatchLicense => "The Client Access License stored by the client has been modified", + Self::BadClientLicense => "The Client Access License stored by the client is in an invalid format", + Self::CantFinishProtocol => "Network problems have caused the licensing protocol to be terminated", + Self::ClientEndedProtocol => "The client prematurely ended the licensing protocol", + Self::BadClientEncryption => "A licensing message was incorrectly encrypted", + Self::CantUpgradeLicense => { + "The Client Access License stored by the client could not be upgraded or renewed" + } + Self::NoRemoteConnections => "The remote computer is not licensed to accept remote connections", + } + } + + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum ProtocolIndependentConnectionBrokerCode { + DestinationNotFound = 0x0000_0400, + LoadingDestination = 0x0000_0402, + RedirectingToDestination = 0x0000_0404, + SessionOnlineVmWake = 0x0000_0405, + SessionOnlineVmBoot = 0x0000_0406, + SessionOnlineVmNoDns = 0x0000_0407, + DestinationPoolNotFree = 0x0000_0408, + ConnectionCancelled = 0x0000_0409, + ConnectionErrorInvalidSettings = 0x0000_0410, + SessionOnlineVmBootTimeout = 0x0000_0411, + SessionOnlineVmSessmonFailed = 0x0000_0412, +} + +impl ProtocolIndependentConnectionBrokerCode { + pub fn description(&self) -> &str { + match self { + Self::DestinationNotFound => "The target endpoint could not be found", + Self::LoadingDestination => "The target endpoint to which the client is being redirected is disconnecting from the Connection Broker", + Self::RedirectingToDestination => "An error occurred while the connection was being redirected to the target endpoint", + Self::SessionOnlineVmWake => "An error occurred while the target endpoint (a virtual machine) was being awakened", + Self::SessionOnlineVmBoot => "An error occurred while the target endpoint (a virtual machine) was being started", + Self::SessionOnlineVmNoDns => "The IP address of the target endpoint (a virtual machine) cannot be determined", + Self::DestinationPoolNotFree => "There are no available endpoints in the pool managed by the Connection Broker", + Self::ConnectionCancelled => "Processing of the connection has been canceled", + Self::ConnectionErrorInvalidSettings => "The settings contained in the routingToken field of the X.224 Connection Request PDU cannot be validated", + Self::SessionOnlineVmBootTimeout => "A time-out occurred while the target endpoint (a virtual machine) was being started", + Self::SessionOnlineVmSessmonFailed => "A session monitoring error occurred while the target endpoint (a virtual machine) was being started", + } + } + + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum RdpSpecificCode { + UnknownPduType2 = 0x0000_10C9, + UnknownPduType = 0x0000_10CA, + DataPdusEquence = 0x0000_10CB, + ControlPduSequence = 0x0000_10CD, + InvalidControlPduAction = 0x0000_10CE, + InvalidInputPduType = 0x0000_10CF, + InvalidInputPduMouse = 0x0000_10D0, + InvalidRefreshRectPdu = 0x0000_10D1, + CreateUserDataFailed = 0x0000_10D2, + ConnectFailed = 0x0000_10D3, + ConfirmActiveWrongShareId = 0x0000_10D4, + ConfirmActiveWrongOriginator = 0x0000_10D5, + PersistentKeyPduBadLength = 0x0000_10DA, + PersistentKeyPduIllegalFirst = 0x0000_10DB, + PersistentKeyPduTooManyTotalKeys = 0x0000_10DC, + PersistentKeyPduTooManyCacheKeys = 0x0000_10DD, + InputPduBadLength = 0x0000_10DE, + BitmapCacheErrorPduBadLength = 0x0000_10DF, + SecurityDataTooShort = 0x0000_10E0, + VcHannelDataTooShort = 0x0000_10E1, + ShareDataTooShort = 0x0000_10E2, + BadSuppressOutputPdu = 0x0000_10E3, + ConfirmActivePduTooShort = 0x0000_10E5, + CapabilitySetTooSmall = 0x0000_10E7, + CapabilitySetTooLarge = 0x0000_10E8, + NoCursorCache = 0x0000_10E9, + BadCapabilities = 0x0000_10EA, + VirtualChannelDecompressionError = 0x0000_10EC, + InvalidVcCompressionType = 0x0000_10ED, + InvalidChannelId = 0x0000_10EF, + VirtualChannelsTooMany = 0x0000_10F0, + RemoteAppsNotEnabled = 0x0000_10F3, + CacheCapabilityNotSet = 0x0000_10F4, + BitmapCacheErrorPduBadLength2 = 0x0000_10F5, + OffscrCacheErrorPduBadLength = 0x0000_10F6, + DngCacheErrorPduBadLength = 0x0000_10F7, + GdiPlusPduBadLength = 0x0000_10F8, + SecurityDataTooShort2 = 0x0000_1111, + SecurityDataTooShort3 = 0x0000_1112, + SecurityDataTooShort4 = 0x0000_1113, + SecurityDataTooShort5 = 0x0000_1114, + SecurityDataTooShort6 = 0x0000_1115, + SecurityDataTooShort7 = 0x0000_1116, + SecurityDataTooShort8 = 0x0000_1117, + SecurityDataTooShort9 = 0x0000_1118, + SecurityDataTooShort10 = 0x0000_1119, + SecurityDataTooShort11 = 0x0000_111A, + SecurityDataTooShort12 = 0x0000_111B, + SecurityDataTooShort13 = 0x0000_111C, + SecurityDataTooShort14 = 0x0000_111D, + SecurityDataTooShort15 = 0x0000_111E, + SecurityDataTooShort16 = 0x0000_111F, + SecurityDataTooShort17 = 0x0000_1120, + SecurityDataTooShort18 = 0x0000_1121, + SecurityDataTooShort19 = 0x0000_1122, + SecurityDataTooShort20 = 0x0000_1123, + SecurityDataTooShort21 = 0x0000_1124, + SecurityDataTooShort22 = 0x0000_1125, + SecurityDataTooShort23 = 0x0000_1126, + BadMonitorData = 0x0000_1129, + VcDecompressedReassembleFailed = 0x0000_112A, + VcDataTooLong = 0x0000_112B, + BadFrameAckData = 0x0000_112C, + GraphicsModeNotSupported = 0x0000_112D, + GraphicsSubsystemResetFailed = 0x0000_112E, + GraphicsSubsystemFailed = 0x0000_112F, + TimezoneKeyNameLengthTooShort = 0x0000_1130, + TimezoneKeyNameLengthTooLong = 0x0000_1131, + DynamicDstDisabledFieldMissing = 0x0000_1132, + VcDecodingError = 0x0000_1133, + VirtualDesktopTooLarge = 0x0000_1134, + MonitorGeometryValidationFailed = 0x0000_1135, + InvalidMonitorCount = 0x0000_1136, + UpdateSessionKeyFailed = 0x0000_1191, + DecryptFailed = 0x0000_1192, + EncryptFailed = 0x0000_1193, + EncPkgMismatch = 0x0000_1194, + DecryptFailed2 = 0x0000_1195, +} + +impl RdpSpecificCode { + pub fn description(&self) -> &str { + match self { + Self::UnknownPduType2 => "Unknown pduType2 field in a received Share Data Header", + Self::UnknownPduType => "Unknown pduType field in a received Share Control Header", + Self::DataPdusEquence => "An out-of-sequence Slow-Path Data PDU has been received", + Self::ControlPduSequence => "An out-of-sequence Slow-Path Non-Data PDU has been received", + Self::InvalidControlPduAction => "A Control PDU has been received with an invalid action field", + Self::InvalidInputPduType => "One of two possible errors: A Slow-Path Input Event has been received with an invalid messageType field; or A Fast-Path Input Event has been received with an invalid eventCode field", + Self::InvalidInputPduMouse => "One of two possible errors: A Slow-Path Mouse Event or Extended Mouse Event has been received with an invalid pointerFlags field; or A Fast-Path Mouse Event or Fast-Path Extended Mouse Event has been received with an invalid pointerFlags field", + Self::InvalidRefreshRectPdu => "An invalid Refresh Rect PDU has been received", + Self::CreateUserDataFailed => "The server failed to construct the GCC Conference Create Response user data", + Self::ConnectFailed => "Processing during the Channel Connection phase of the RDP Connection Sequence has failed", + Self::ConfirmActiveWrongShareId => "A Confirm Active PDU was received from the client with an invalid shareID field", + Self::ConfirmActiveWrongOriginator => "A Confirm Active PDU was received from the client with an invalid originatorID field", + Self::PersistentKeyPduBadLength => "There is not enough data to process a Persistent Key List PDU", + Self::PersistentKeyPduIllegalFirst => "A Persistent Key List PDU marked as PERSIST_PDU_FIRST (0x01) was received after the reception of a prior Persistent Key List PDU also marked as PERSIST_PDU_FIRST", + Self::PersistentKeyPduTooManyTotalKeys => "A Persistent Key List PDU was received which specified a total number of bitmap cache entries larger than 262144", + Self::PersistentKeyPduTooManyCacheKeys => "A Persistent Key List PDU was received which specified an invalid total number of keys for a bitmap cache (the number of entries that can be stored within each bitmap cache is specified in the Revision 1 or 2 Bitmap Cache Capability Set that is sent from client to server)", + Self::InputPduBadLength => "There is not enough data to process Input Event PDU Data or a Fast-Path Input Event PDU", + Self::BitmapCacheErrorPduBadLength => "There is not enough data to process the shareDataHeader, NumInfoBlocks, Pad1, and Pad2 fields of the Bitmap Cache Error PDU Data", + Self::SecurityDataTooShort => "One of two possible errors: The dataSignature field of the Fast-Path Input Event PDU does not contain enough data; or The fipsInformation and dataSignature fields of the Fast-Path Input Event PDU do not contain enough data", + Self::VcHannelDataTooShort => "One of two possible errors: There is not enough data in the Client Network Data to read the virtual channel configuration data; or There is not enough data to read a complete Channel PDU Header", + Self::ShareDataTooShort => "One of four possible errors: There is not enough data to process Control PDU Data; or There is not enough data to read a complete Share Control Header; or There is not enough data to read a complete Share Data Header of a Slow-Path Data PDU; or There is not enough data to process Font List PDU Data", + Self::BadSuppressOutputPdu => "One of two possible errors: There is not enough data to process Suppress Output PDU Data; or The allowDisplayUpdates field of the Suppress Output PDU Data is invalid", + Self::ConfirmActivePduTooShort => "One of two possible errors: There is not enough data to read the shareControlHeader, shareID, originatorID, lengthSourceDescriptor, and lengthCombinedCapabilities fields of the Confirm Active PDU Data; or There is not enough data to read the sourceDescriptor, numberCapabilities, pad2Octets, and capabilitySets fields of the Confirm Active PDU Data", + Self::CapabilitySetTooSmall => "There is not enough data to read the capabilitySetType and the lengthCapability fields in a received Capability Set", + Self::CapabilitySetTooLarge => "A Capability Set has been received with a lengthCapability field that contains a value greater than the total length of the data received", + Self::NoCursorCache => "One of two possible errors: Both the colorPointerCacheSize and pointerCacheSize fields in the Pointer Capability Set are set to zero; or The pointerCacheSize field in the Pointer Capability Set is not present, and the colorPointerCacheSize field is set to zero", + Self::BadCapabilities => "The capabilities received from the client in the Confirm Active PDU were not accepted by the server", + Self::VirtualChannelDecompressionError => "An error occurred while using the bulk compressor to decompress a Virtual Channel PDU", + Self::InvalidVcCompressionType => "An invalid bulk compression package was specified in the flags field of the Channel PDU Header", + Self::InvalidChannelId => "An invalid MCS channel ID was specified in the mcsPdu field of the Virtual Channel PDU)", + Self::VirtualChannelsTooMany => "The client requested more than the maximum allowed 31 static virtual channels in the Client Network Data", + Self::RemoteAppsNotEnabled => "The INFO_RAIL flag (0x0000_8000) MUST be set in the flags field of the Info Packet as the session on the remote server can only host remote applications", + Self::CacheCapabilityNotSet => "The client sent a Persistent Key List PDU without including the prerequisite Revision 2 Bitmap Cache Capability Set in the Confirm Active PDU", + Self::BitmapCacheErrorPduBadLength2 => "The NumInfoBlocks field in the Bitmap Cache Error PDU Data is inconsistent with the amount of data in the Info field", + Self::OffscrCacheErrorPduBadLength => "There is not enough data to process an Offscreen Bitmap Cache Error PDU", + Self::DngCacheErrorPduBadLength => "There is not enough data to process a DrawNineGrid Cache Error PDU", + Self::GdiPlusPduBadLength => "There is not enough data to process a GDI+ Error PDU", + Self::SecurityDataTooShort2 => "There is not enough data to read a Basic Security Header", + Self::SecurityDataTooShort3 => "There is not enough data to read a Non-FIPS Security Header or FIPS Security Header", + Self::SecurityDataTooShort4 => "There is not enough data to read the basicSecurityHeader and length fields of the Security Exchange PDU Data", + Self::SecurityDataTooShort5 => "There is not enough data to read the CodePage, flags, cbDomain, cbUserName, cbPassword, cbAlternateShell, cbWorkingDir, Domain, UserName, Password, AlternateShell, and WorkingDir fields in the Info Packet", + Self::SecurityDataTooShort6 => "There is not enough data to read the CodePage, flags, cbDomain, cbUserName, cbPassword, cbAlternateShell, and cbWorkingDir fields in the Info Packet", + Self::SecurityDataTooShort7 => "There is not enough data to read the clientAddressFamily and cbClientAddress fields in the Extended Info Packet", + Self::SecurityDataTooShort8 => "There is not enough data to read the clientAddress field in the Extended Info Packet", + Self::SecurityDataTooShort9 => "There is not enough data to read the cbClientDir field in the Extended Info Packet", + Self::SecurityDataTooShort10 => "There is not enough data to read the clientDir field in the Extended Info Packet", + Self::SecurityDataTooShort11 => "There is not enough data to read the clientTimeZone field in the Extended Info Packet", + Self::SecurityDataTooShort12 => "There is not enough data to read the clientSessionId field in the Extended Info Packet", + Self::SecurityDataTooShort13 => "There is not enough data to read the performanceFlags field in the Extended Info Packet", + Self::SecurityDataTooShort14 => "There is not enough data to read the cbAutoReconnectCookie field in the Extended Info Packet", + Self::SecurityDataTooShort15 => "There is not enough data to read the autoReconnectCookie field in the Extended Info Packet", + Self::SecurityDataTooShort16 => "The cbAutoReconnectCookie field in the Extended Info Packet contains a value which is larger than the maximum allowed length of 128 bytes", + Self::SecurityDataTooShort17 => "There is not enough data to read the clientAddressFamily and cbClientAddress fields in the Extended Info Packet", + Self::SecurityDataTooShort18 => "There is not enough data to read the clientAddress field in the Extended Info Packet", + Self::SecurityDataTooShort19 => "There is not enough data to read the cbClientDir field in the Extended Info Packet", + Self::SecurityDataTooShort20 => "There is not enough data to read the clientDir field in the Extended Info Packet", + Self::SecurityDataTooShort21 => "There is not enough data to read the clientTimeZone field in the Extended Info Packet", + Self::SecurityDataTooShort22 => "There is not enough data to read the clientSessionId field in the Extended Info Packet", + Self::SecurityDataTooShort23 => "There is not enough data to read the Client Info PDU Data", + Self::BadMonitorData => "The number of TS_MONITOR_DEF structures present in the monitorDefArray field of the Client Monitor Data is less than the value specified in monitorCount field", + Self::VcDecompressedReassembleFailed => "The server-side decompression buffer is invalid, or the size of the decompressed VC data exceeds the chunking size specified in the Virtual Channel Capability Set", + Self::VcDataTooLong => "The size of a received Virtual Channel PDU exceeds the chunking size specified in the Virtual Channel Capability Set", + Self::BadFrameAckData => "There is not enough data to read a TS_FRAME_ACKNOWLEDGE_PDU", + Self::GraphicsModeNotSupported => "The graphics mode requested by the client is not supported by the server", + Self::GraphicsSubsystemResetFailed => "The server-side graphics subsystem failed to reset", + Self::GraphicsSubsystemFailed => "The server-side graphics subsystem is in an error state and unable to continue graphics encoding", + Self::TimezoneKeyNameLengthTooShort => "There is not enough data to read the cbDynamicDSTTimeZoneKeyName field in the Extended Info Packet", + Self::TimezoneKeyNameLengthTooLong => "The length reported in the cbDynamicDSTTimeZoneKeyName field of the Extended Info Packet is too long", + Self::DynamicDstDisabledFieldMissing => "The dynamicDaylightTimeDisabled field is not present in the Extended Info Packet", + Self::VcDecodingError => "An error occurred when processing dynamic virtual channel data", + Self::VirtualDesktopTooLarge => "The width or height of the virtual desktop defined by the monitor layout in the Client Monitor Data is larger than the maximum allowed value of 32,766", + Self::MonitorGeometryValidationFailed => "The monitor geometry defined by the Client Monitor Data is invalid", + Self::InvalidMonitorCount => "The monitorCount field in the Client Monitor Data is too large", + Self::UpdateSessionKeyFailed => "An attempt to update the session keys while using Standard RDP Security mechanisms failed", + Self::DecryptFailed => "One of two possible error conditions: Decryption using Standard RDP Security mechanisms failed; or Session key creation using Standard RDP Security mechanisms failed", + Self::EncryptFailed => "Encryption using Standard RDP Security mechanisms failed", + Self::EncPkgMismatch => "Failed to find a usable Encryption Method in the encryptionMethods field of the Client Security Data", + Self::DecryptFailed2 => "Unencrypted data was encountered in a protocol stream which is meant to be encrypted with Standard RDP Security mechanisms", + } + } + + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[cfg(test)] +mod tests { + use ironrdp_core::{decode, encode_vec}; + + use super::*; + + const SERVER_SET_ERROR_INFO_BUFFER: [u8; 4] = [0x00, 0x01, 0x00, 0x00]; + + const SERVER_SET_ERROR_INFO: ServerSetErrorInfoPdu = ServerSetErrorInfoPdu( + ErrorInfo::ProtocolIndependentLicensingCode(ProtocolIndependentLicensingCode::Internal), + ); + + #[test] + fn from_buffer_correctly_parses_server_set_error_info() { + assert_eq!( + SERVER_SET_ERROR_INFO, + decode(SERVER_SET_ERROR_INFO_BUFFER.as_ref()).unwrap() + ); + } + + #[test] + fn to_buffer_correctly_serializes_server_set_error_info() { + let expected = SERVER_SET_ERROR_INFO_BUFFER.as_ref(); + + let buffer = encode_vec(&SERVER_SET_ERROR_INFO).unwrap(); + assert_eq!(expected, buffer.as_slice()); + } + + #[test] + fn buffer_length_is_correct_for_server_set_error_info() { + assert_eq!(SERVER_SET_ERROR_INFO_BUFFER.len(), SERVER_SET_ERROR_INFO.size()); + } +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/client_license_info.rs b/crates/ironrdp-pdu/src/rdp/server_license/client_license_info.rs new file mode 100644 index 00000000..4c72032d --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/client_license_info.rs @@ -0,0 +1,207 @@ +use std::io; + +use byteorder::{LittleEndian, WriteBytesExt as _}; +use ironrdp_core::{ + ensure_size, invalid_field_err, Decode as _, DecodeResult, Encode as _, EncodeResult, ReadCursor, WriteCursor, +}; +use md5::Digest as _; + +use crate::crypto::rc4::Rc4; +use crate::crypto::rsa::encrypt_with_public_key; +use crate::rdp::headers::{BasicSecurityHeader, BasicSecurityHeaderFlags}; +use crate::rdp::server_license::client_new_license_request::{compute_master_secret, compute_session_key_blob}; +use crate::rdp::server_license::client_platform_challenge_response::CLIENT_HARDWARE_IDENTIFICATION_SIZE; +use crate::rdp::server_license::{ + compute_mac_data, BlobHeader, BlobType, LicenseEncryptionData, LicenseHeader, PreambleFlags, PreambleType, + PreambleVersion, ServerLicenseError, ServerLicenseRequest, KEY_EXCHANGE_ALGORITHM_RSA, MAC_SIZE, PLATFORM_ID, + PREAMBLE_SIZE, RANDOM_NUMBER_SIZE, +}; + +const LICENSE_INFO_STATIC_FIELDS_SIZE: usize = 20; + +/// [2.2.2.3] Client License Info (CLIENT_LICENSE_INFO) +/// +/// [2.2.2.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/9407b2eb-f180-4827-9488-cdbff4a5d4ea +#[derive(Debug, PartialEq, Eq)] +pub struct ClientLicenseInfo { + pub license_header: LicenseHeader, + pub client_random: Vec, + pub encrypted_premaster_secret: Vec, + pub license_info: Vec, + pub encrypted_hwid: Vec, + pub mac_data: Vec, +} + +impl ClientLicenseInfo { + const NAME: &'static str = "ClientLicenseInfo"; + + pub fn from_server_license_request( + license_request: &ServerLicenseRequest, + client_random: &[u8], + premaster_secret: &[u8], + hardware_data: [u32; 4], + license_info: Vec, + ) -> Result<(Self, LicenseEncryptionData), ServerLicenseError> { + let public_key = license_request.get_public_key()? + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, + "attempted to retrieve the server public key from a server license request message that does not have a certificate"))?; + let encrypted_premaster_secret = encrypt_with_public_key(premaster_secret, &public_key)?; + + let master_secret = compute_master_secret( + premaster_secret, + client_random, + license_request.server_random.as_slice(), + ); + let session_key_blob = compute_session_key_blob( + master_secret.as_slice(), + client_random, + license_request.server_random.as_slice(), + ); + let mac_salt_key = &session_key_blob[..16]; + + let mut md5 = md5::Md5::new(); + md5.update( + [ + &session_key_blob[16..32], + client_random, + license_request.server_random.as_slice(), + ] + .concat() + .as_slice(), + ); + let license_key = md5.finalize().to_vec(); + + let mut hardware_id = Vec::with_capacity(CLIENT_HARDWARE_IDENTIFICATION_SIZE); + hardware_id.write_u32::(PLATFORM_ID)?; + for data in hardware_data { + hardware_id.write_u32::(data)?; + } + + let mut rc4 = Rc4::new(&license_key); + let encrypted_hwid = rc4.process(&hardware_id); + + let mac_data = compute_mac_data(mac_salt_key, &hardware_id)?; + + let size = RANDOM_NUMBER_SIZE + + PREAMBLE_SIZE + + LICENSE_INFO_STATIC_FIELDS_SIZE + + encrypted_premaster_secret.len() + + license_info.len() + + encrypted_hwid.len() + + MAC_SIZE; + + let license_header = LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::LicenseInfo, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: u16::try_from(size) + .map_err(|_| ServerLicenseError::InvalidField("preamble message size"))?, + }; + + Ok(( + Self { + license_header, + client_random: Vec::from(client_random), + encrypted_premaster_secret, + license_info, + encrypted_hwid, + mac_data, + }, + LicenseEncryptionData { + premaster_secret: Vec::from(premaster_secret), + mac_salt_key: Vec::from(mac_salt_key), + license_key, + }, + )) + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.license_header.encode(dst)?; + + dst.write_u32(KEY_EXCHANGE_ALGORITHM_RSA); + dst.write_u32(PLATFORM_ID); + dst.write_slice(&self.client_random); + + BlobHeader::new(BlobType::RANDOM, self.encrypted_premaster_secret.len()).encode(dst)?; + dst.write_slice(&self.encrypted_premaster_secret); + + BlobHeader::new(BlobType::DATA, self.license_info.len()).encode(dst)?; + dst.write_slice(&self.license_info); + + BlobHeader::new(BlobType::ENCRYPTED_DATA, self.encrypted_hwid.len()).encode(dst)?; + dst.write_slice(&self.encrypted_hwid); + + dst.write_slice(&self.mac_data); + + Ok(()) + } + + pub fn decode(license_header: LicenseHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + if license_header.preamble_message_type != PreambleType::LicenseInfo { + return Err(invalid_field_err!("preambleMessageType", "unexpected preamble type")); + } + + let key_exchange_algorithm = src.read_u32(); + if key_exchange_algorithm != KEY_EXCHANGE_ALGORITHM_RSA { + return Err(invalid_field_err!("keyExchangeAlgo", "invalid key exchange algorithm")); + } + + // We can ignore platform ID + let _platform_id = src.read_u32(); + + ensure_size!(in: src, size: RANDOM_NUMBER_SIZE); + let client_random = src.read_slice(RANDOM_NUMBER_SIZE).into(); + + let premaster_secret_blob_header = BlobHeader::decode(src)?; + if premaster_secret_blob_header.blob_type != BlobType::RANDOM { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: premaster_secret_blob_header.length); + let encrypted_premaster_secret = src.read_slice(premaster_secret_blob_header.length).into(); + + let license_info_blob_header = BlobHeader::decode(src)?; + if license_info_blob_header.blob_type != BlobType::DATA { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: license_info_blob_header.length); + let license_info = src.read_slice(license_info_blob_header.length).into(); + + let encrypted_hwid_blob_header = BlobHeader::decode(src)?; + if encrypted_hwid_blob_header.blob_type != BlobType::DATA { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: encrypted_hwid_blob_header.length); + let encrypted_hwid = src.read_slice(encrypted_hwid_blob_header.length).into(); + + ensure_size!(in: src, size: MAC_SIZE); + let mac_data = src.read_slice(MAC_SIZE).into(); + + Ok(Self { + license_header, + client_random, + encrypted_premaster_secret, + license_info, + encrypted_hwid, + mac_data, + }) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + self.license_header.size() + + LICENSE_INFO_STATIC_FIELDS_SIZE + + RANDOM_NUMBER_SIZE + + self.encrypted_premaster_secret.len() + + self.license_info.len() + + self.encrypted_hwid.len() + + MAC_SIZE + } +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request/mod.rs b/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request/mod.rs new file mode 100644 index 00000000..adaacc86 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request/mod.rs @@ -0,0 +1,250 @@ +#[cfg(test)] +mod tests; + +use std::io; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_size, invalid_field_err, Decode as _, DecodeResult, Encode as _, EncodeResult, ReadCursor, WriteCursor, +}; +use md5::Digest as _; + +use super::{ + BasicSecurityHeader, BasicSecurityHeaderFlags, BlobHeader, BlobType, LicenseEncryptionData, LicenseHeader, + PreambleFlags, PreambleType, PreambleVersion, ServerLicenseError, ServerLicenseRequest, KEY_EXCHANGE_ALGORITHM_RSA, + PREAMBLE_SIZE, RANDOM_NUMBER_SIZE, UTF8_NULL_TERMINATOR_SIZE, +}; +use crate::crypto::rsa::encrypt_with_public_key; +use crate::utils::{self, CharacterSet}; + +const LICENSE_REQUEST_STATIC_FIELDS_SIZE: usize = 20; + +pub const PLATFORM_ID: u32 = ClientOsType::NT_POST_52.bits() | Isv::MICROSOFT.bits(); + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ClientOsType: u32 { + const NT_351 = 0x100_0000; + const NT_40 = 0x200_0000; + const NT_50 = 0x300_0000; + const NT_POST_52 = 0x400_0000; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Isv: u32 { + const MICROSOFT = 0x10000; + const CITRIX = 0x20000; + } +} + +/// [2.2.2.2] Client New License Request (CLIENT_NEW_LICENSE_REQUEST) +/// +/// [2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/c57e4890-9049-421e-9fe8-9a6f9519675a +#[derive(Debug, PartialEq, Eq)] +pub struct ClientNewLicenseRequest { + pub license_header: LicenseHeader, + pub client_random: Vec, + pub encrypted_premaster_secret: Vec, + pub client_username: String, + pub client_machine_name: String, +} + +impl ClientNewLicenseRequest { + const NAME: &'static str = "ClientNewLicenseRequest"; + + pub fn from_server_license_request( + license_request: &ServerLicenseRequest, + client_random: &[u8], + premaster_secret: &[u8], + client_username: &str, + client_machine_name: &str, + ) -> Result<(Self, LicenseEncryptionData), ServerLicenseError> { + let public_key = license_request.get_public_key()? + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, + "attempted to retrieve the server public key from a server license request message that does not have a certificate"))?; + + let encrypted_premaster_secret = encrypt_with_public_key(premaster_secret, &public_key)?; + + let master_secret = compute_master_secret( + premaster_secret, + client_random, + license_request.server_random.as_slice(), + ); + let session_key_blob = compute_session_key_blob( + master_secret.as_slice(), + client_random, + license_request.server_random.as_slice(), + ); + let mac_salt_key = &session_key_blob[..16]; + + let mut md5 = md5::Md5::new(); + md5.update( + [ + &session_key_blob[16..32], + client_random, + license_request.server_random.as_slice(), + ] + .concat() + .as_slice(), + ); + let license_key = md5.finalize().to_vec(); + + let license_header = LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::NewLicenseRequest, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: u16::try_from( + RANDOM_NUMBER_SIZE + + PREAMBLE_SIZE + + LICENSE_REQUEST_STATIC_FIELDS_SIZE + + encrypted_premaster_secret.len() + + client_machine_name.len() + + UTF8_NULL_TERMINATOR_SIZE + + client_username.len() + + UTF8_NULL_TERMINATOR_SIZE, + ) + .map_err(|_| ServerLicenseError::InvalidField("preamble message size"))?, + }; + + Ok(( + Self { + license_header, + client_random: Vec::from(client_random), + encrypted_premaster_secret, + client_username: client_username.to_owned(), + client_machine_name: client_machine_name.to_owned(), + }, + LicenseEncryptionData { + premaster_secret: Vec::from(premaster_secret), + mac_salt_key: Vec::from(mac_salt_key), + license_key, + }, + )) + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.license_header.encode(dst)?; + + dst.write_u32(KEY_EXCHANGE_ALGORITHM_RSA); + dst.write_u32(PLATFORM_ID); + dst.write_slice(&self.client_random); + + BlobHeader::new(BlobType::RANDOM, self.encrypted_premaster_secret.len()).encode(dst)?; + dst.write_slice(&self.encrypted_premaster_secret); + + BlobHeader::new( + BlobType::CLIENT_USER_NAME, + self.client_username.len() + UTF8_NULL_TERMINATOR_SIZE, + ) + .encode(dst)?; + utils::write_string_to_cursor(dst, &self.client_username, CharacterSet::Ansi, true)?; + + BlobHeader::new( + BlobType::CLIENT_MACHINE_NAME_BLOB, + self.client_machine_name.len() + UTF8_NULL_TERMINATOR_SIZE, + ) + .encode(dst)?; + utils::write_string_to_cursor(dst, &self.client_machine_name, CharacterSet::Ansi, true)?; + + Ok(()) + } + + pub fn decode(license_header: LicenseHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + if license_header.preamble_message_type != PreambleType::NewLicenseRequest { + return Err(invalid_field_err!("preambleMessageType", "unexpected preamble type")); + } + + ensure_size!(in: src, size: LICENSE_REQUEST_STATIC_FIELDS_SIZE + RANDOM_NUMBER_SIZE); + let key_exchange_algorithm = src.read_u32(); + if key_exchange_algorithm != KEY_EXCHANGE_ALGORITHM_RSA { + return Err(invalid_field_err!("keyExchangeAlgo", "invalid key exchange algorithm")); + } + + let _platform_id = src.read_u32(); + let client_random = src.read_slice(RANDOM_NUMBER_SIZE).into(); + + let premaster_secret_blob_header = BlobHeader::decode(src)?; + if premaster_secret_blob_header.blob_type != BlobType::RANDOM { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: premaster_secret_blob_header.length); + let encrypted_premaster_secret = src.read_slice(premaster_secret_blob_header.length).into(); + + let username_blob_header = BlobHeader::decode(src)?; + if username_blob_header.blob_type != BlobType::CLIENT_USER_NAME { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: username_blob_header.length); + let client_username = + utils::decode_string(src.read_slice(username_blob_header.length), CharacterSet::Ansi, false)?; + + let machine_name_blob = BlobHeader::decode(src)?; + if machine_name_blob.blob_type != BlobType::CLIENT_MACHINE_NAME_BLOB { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: machine_name_blob.length); + let client_machine_name = + utils::decode_string(src.read_slice(machine_name_blob.length), CharacterSet::Ansi, false)?; + + Ok(Self { + license_header, + client_random, + encrypted_premaster_secret, + client_username, + client_machine_name, + }) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + self.license_header.size() + + LICENSE_REQUEST_STATIC_FIELDS_SIZE + + RANDOM_NUMBER_SIZE + + self.encrypted_premaster_secret.len() + + self.client_machine_name.len() + + UTF8_NULL_TERMINATOR_SIZE + + self.client_username.len() + + UTF8_NULL_TERMINATOR_SIZE + } +} + +fn salted_hash(salt: &[u8], salt_first: &[u8], salt_second: &[u8], input: &[u8]) -> Vec { + let mut hasher = sha1::Sha1::new(); + hasher.update([input, salt, salt_first, salt_second].concat().as_slice()); + let sha_result = hasher.finalize(); + + let mut md5 = md5::Md5::new(); + md5.update([salt, sha_result.as_ref()].concat().as_slice()); + + md5.finalize().to_vec() +} + +// According to https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/88061224-4a2f-4a28-a52e-e896b75ed2d3 +pub(crate) fn compute_master_secret(premaster_secret: &[u8], client_random: &[u8], server_random: &[u8]) -> Vec { + [ + salted_hash(premaster_secret, client_random, server_random, b"A"), + salted_hash(premaster_secret, client_random, server_random, b"BB"), + salted_hash(premaster_secret, client_random, server_random, b"CCC"), + ] + .concat() +} + +// According to https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/88061224-4a2f-4a28-a52e-e896b75ed2d3 +pub(crate) fn compute_session_key_blob(master_secret: &[u8], client_random: &[u8], server_random: &[u8]) -> Vec { + [ + salted_hash(master_secret, server_random, client_random, b"A"), + salted_hash(master_secret, server_random, client_random, b"BB"), + salted_hash(master_secret, server_random, client_random, b"CCC"), + ] + .concat() +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request/tests.rs b/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request/tests.rs new file mode 100644 index 00000000..8d54c706 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request/tests.rs @@ -0,0 +1,377 @@ +use std::sync::LazyLock; + +use byteorder::{LittleEndian, WriteBytesExt as _}; +use ironrdp_core::{decode, encode_vec}; + +use super::*; +use crate::rdp::server_license::server_license_request::cert::{CertificateType, X509CertificateChain}; +use crate::rdp::server_license::server_license_request::{ProductInfo, Scope, ServerCertificate}; +use crate::rdp::server_license::LicensePdu; + +const LICENSE_HEADER_BUFFER_NO_SIZE: [u8; 6] = [ + 0x80, 0x00, // flags + 0x00, 0x00, // flagsHi + 0x13, 0x03, +]; + +const CLIENT_RANDOM_BUFFER: [u8; 32] = [ + 0x4b, 0x5b, 0x7b, 0x43, 0x63, 0x8a, 0x8, 0xfe, 0xd1, 0x7a, 0xba, 0xf5, 0x91, 0x85, 0x77, 0xfe, 0x39, 0x36, 0xf6, + 0xd7, 0x78, 0xec, 0x6a, 0xcc, 0x89, 0x4a, 0x90, 0x41, 0x2c, 0xac, 0x5a, 0x49, +]; + +const SERVER_RANDOM_BUFFER: [u8; 32] = [ + 0x5c, 0x81, 0xf0, 0x11, 0xeb, 0xcf, 0xd1, 0xe, 0xb4, 0x1f, 0xb3, 0xba, 0x93, 0xa2, 0xd7, 0x39, 0x9, 0xaa, 0x99, + 0xe9, 0x10, 0xd4, 0xd7, 0x95, 0xdd, 0xad, 0x91, 0x69, 0x5, 0x26, 0x6b, 0x6a, +]; + +const ENCRYPTED_PREMASTER_SECRET: [u8; 72] = [ + 0xb0, 0x95, 0xf7, 0xcb, 0x81, 0x34, 0x45, 0x85, 0x65, 0x83, 0xb2, 0xcf, 0x5b, 0xdf, 0xfe, 0x40, 0x6f, 0xd8, 0x14, + 0x14, 0x18, 0xb, 0x2b, 0x6d, 0xaa, 0xd0, 0x38, 0xa, 0xaf, 0x74, 0xe9, 0x51, 0x55, 0x2b, 0xdb, 0xfd, 0x3b, 0xd3, + 0xfb, 0x2f, 0x34, 0x92, 0xdd, 0xc8, 0xaf, 0x48, 0xf3, 0x91, 0x61, 0x8a, 0x5b, 0xbd, 0x81, 0x87, 0xec, 0xb8, 0xcc, + 0xb, 0xb, 0xc9, 0xd, 0x1c, 0xe7, 0x17, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +const PREMASTER_SECRET_BUFFER: [u8; 48] = [ + 0x14, 0x28, 0xda, 0xfb, 0xb9, 0xea, 0x38, 0xab, 0x5e, 0xa2, 0xf9, 0x4, 0xf7, 0x89, 0x9c, 0x98, 0x3d, 0x50, 0x45, + 0x77, 0xbf, 0x17, 0x81, 0x1c, 0x37, 0x87, 0xc2, 0x48, 0x13, 0xe8, 0xc9, 0x20, 0x4d, 0xdf, 0xf3, 0x27, 0xbd, 0xb6, + 0x98, 0x7e, 0x64, 0xda, 0xfe, 0x1d, 0x31, 0x2f, 0x62, 0xca, +]; + +const SALTED_HASH_BUFFER: [u8; 16] = [ + 0xfe, 0xdc, 0x51, 0x9a, 0xdb, 0x3a, 0xc9, 0x61, 0x4, 0x7, 0x24, 0x94, 0x5d, 0xc, 0x43, 0xa7, +]; + +const MASTER_SECRET_BUFFER: [u8; 48] = [ + 0xfe, 0xdc, 0x51, 0x9a, 0xdb, 0x3a, 0xc9, 0x61, 0x4, 0x7, 0x24, 0x94, 0x5d, 0xc, 0x43, 0xa7, 0x70, 0xe3, 0xf3, 0x0, + 0x50, 0xd7, 0xa8, 0x72, 0x3e, 0xab, 0x7e, 0x1b, 0xe4, 0x64, 0xe5, 0xc5, 0x74, 0xae, 0xed, 0x10, 0x72, 0x96, 0x2a, + 0x4c, 0x65, 0x9, 0x4f, 0x60, 0x12, 0xa9, 0x12, 0xa1, +]; + +const SESSION_KEY_BLOB: [u8; 48] = [ + 0xf7, 0x3, 0x75, 0xb9, 0x5f, 0xda, 0xd0, 0xbe, 0xb4, 0x2a, 0xf5, 0xc1, 0x3d, 0x98, 0x85, 0x7a, 0xd6, 0xc5, 0x39, + 0x4c, 0xe3, 0xcb, 0x76, 0x61, 0xaa, 0x4a, 0xb6, 0x15, 0x7e, 0x89, 0x21, 0x3d, 0xdf, 0x5b, 0x25, 0x32, 0xee, 0x5, + 0x6, 0xd, 0x5b, 0xaa, 0x63, 0x14, 0xaf, 0xa5, 0x46, 0xf, +]; + +const LICENSE_KEY_BUFFER: [u8; 16] = [ + 0xfa, 0x44, 0xe8, 0x78, 0xd8, 0x2b, 0x3f, 0x1d, 0x4d, 0x0, 0xa0, 0xa6, 0x55, 0xce, 0x8a, 0xb7, +]; + +const CLIENT_USERNAME: &str = "sample-user"; +const CLIENT_MACHINE_NAME: &str = "sample-machine-name"; + +static CLIENT_NEW_LICENSE_REQUEST: LazyLock = LazyLock::new(|| { + ClientNewLicenseRequest { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::NewLicenseRequest, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: u16::try_from( + PREAMBLE_SIZE + + RANDOM_NUMBER_SIZE + + LICENSE_REQUEST_STATIC_FIELDS_SIZE + + ENCRYPTED_PREMASTER_SECRET.len() + + CLIENT_MACHINE_NAME.len() + + UTF8_NULL_TERMINATOR_SIZE + + CLIENT_USERNAME.len() + + UTF8_NULL_TERMINATOR_SIZE, + ) + .expect("can't panic"), + }, + client_random: Vec::from(CLIENT_RANDOM_BUFFER.as_ref()), + encrypted_premaster_secret: Vec::from(ENCRYPTED_PREMASTER_SECRET.as_ref()), + client_username: CLIENT_USERNAME.to_owned(), + client_machine_name: CLIENT_MACHINE_NAME.to_owned(), + } + .into() +}); + +static REQUEST_BUFFER: LazyLock> = LazyLock::new(|| { + let username_len = CLIENT_USERNAME.len() + UTF8_NULL_TERMINATOR_SIZE; + let mut username_len_buf = Vec::new(); + username_len_buf + .write_u16::(u16::try_from(username_len).expect("can't panic")) + .unwrap(); + + let machine_name_len = CLIENT_MACHINE_NAME.len() + UTF8_NULL_TERMINATOR_SIZE; + let mut machine_name_len_buf = Vec::new(); + machine_name_len_buf + .write_u16::(u16::try_from(machine_name_len).unwrap()) + .unwrap(); + + let buf = [ + &[ + 0x01u8, 0x00, 0x00, 0x00, // preferred_key_exchange_algorithm + 0x00, 0x00, 0x01, 0x04, + ], // platform_id + CLIENT_RANDOM_BUFFER.as_ref(), + &[ + 0x02, 0x00, // blob type + 0x48, 0x00, + ], // blob len + ENCRYPTED_PREMASTER_SECRET.as_ref(), + &[0x0f, 0x00], // blob type + username_len_buf.as_slice(), + CLIENT_USERNAME.as_bytes(), + &[ + 0x00, // null + 0x10, 0x00, + ], // blob type + machine_name_len_buf.as_slice(), // blob len + CLIENT_MACHINE_NAME.as_bytes(), + &[0x00], + ] // null + .concat(); + + let preamble_size_field = u16::try_from(buf.len() + PREAMBLE_SIZE).expect("can't panic"); + + [ + LICENSE_HEADER_BUFFER_NO_SIZE.as_ref(), + &preamble_size_field.to_le_bytes(), + buf.as_slice(), + ] + .concat() +}); + +pub(crate) static SERVER_LICENSE_REQUEST: LazyLock = LazyLock::new(|| { + let mut req = ServerLicenseRequest { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::LicenseRequest, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: 0, + }, + server_random: Vec::from(SERVER_RANDOM_BUFFER.as_ref()), + product_info: ProductInfo { + version: 0x60000, + company_name: "Microsoft Corporation".to_owned(), + product_id: "A02".to_owned(), + }, + server_certificate: Some(ServerCertificate { + issued_permanently: true, + certificate: CertificateType::X509(X509CertificateChain { + certificate_array: vec![ + vec![ + 0x30, 0x82, 0x03, 0xda, 0x30, 0x82, 0x02, 0xc2, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x13, 0x7f, + 0x00, 0x00, 0x01, 0x76, 0x00, 0x8f, 0x08, 0x64, 0x08, 0x68, 0xa7, 0x63, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x76, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, + 0x00, 0x30, 0x1d, 0x31, 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x12, 0x50, 0x72, + 0x6f, 0x64, 0x32, 0x4c, 0x53, 0x52, 0x41, 0x73, 0x68, 0x61, 0x32, 0x52, 0x44, 0x53, 0x4c, 0x4d, + 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x39, 0x31, 0x30, 0x32, 0x36, 0x32, 0x32, 0x35, 0x33, 0x34, 0x30, + 0x5a, 0x17, 0x0d, 0x32, 0x37, 0x30, 0x36, 0x30, 0x36, 0x32, 0x30, 0x34, 0x32, 0x33, 0x38, 0x5a, + 0x30, 0x11, 0x31, 0x0f, 0x30, 0x0d, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x06, 0x42, 0x65, 0x63, + 0x6b, 0x65, 0x72, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, + 0x82, 0x01, 0x01, 0x00, 0xa8, 0x6b, 0xda, 0xae, 0x08, 0x1d, 0xc5, 0x05, 0x70, 0x7d, 0xa0, 0x41, + 0x46, 0xb4, 0x14, 0xcf, 0xfb, 0x8e, 0x09, 0x0b, 0x0a, 0x52, 0x8a, 0x7f, 0x7a, 0x35, 0xb6, 0xe3, + 0x0d, 0x1c, 0xbe, 0x49, 0x63, 0x41, 0x92, 0x86, 0x00, 0xa2, 0xd3, 0xff, 0x5b, 0x08, 0x7d, 0x2b, + 0x65, 0xe4, 0xc3, 0x09, 0x68, 0x72, 0x21, 0xc4, 0xd8, 0x0a, 0x21, 0x9e, 0x1f, 0xdf, 0xb2, 0xaa, + 0x2b, 0x42, 0x68, 0xe7, 0xeb, 0x52, 0xf8, 0x9e, 0xfc, 0x7f, 0x0f, 0x55, 0x26, 0x7d, 0x44, 0xfb, + 0x35, 0xe5, 0xc2, 0x2c, 0xb6, 0x8d, 0x06, 0xc5, 0xdc, 0xbf, 0x66, 0xf6, 0xb2, 0xf2, 0x9b, 0xe2, + 0x49, 0xaf, 0xfd, 0x4c, 0x69, 0x46, 0x72, 0xe0, 0x2f, 0x31, 0x77, 0x86, 0x7b, 0x5b, 0x6d, 0x49, + 0xe6, 0xc7, 0x84, 0xd1, 0xdd, 0x56, 0x89, 0x8d, 0xbd, 0x07, 0x18, 0x01, 0x43, 0x70, 0x9b, 0x00, + 0x71, 0x16, 0x89, 0x66, 0x2e, 0xb6, 0x5f, 0x62, 0xeb, 0x96, 0xed, 0xf2, 0xdb, 0xdb, 0xcf, 0xdd, + 0xa8, 0xab, 0xde, 0x93, 0xb3, 0xdb, 0x54, 0xf0, 0x34, 0x4a, 0x28, 0xc3, 0x11, 0xf6, 0xb9, 0xd6, + 0x45, 0x3f, 0x07, 0xc0, 0x8e, 0x10, 0x7a, 0x2b, 0x56, 0x15, 0xbb, 0x00, 0x9d, 0x82, 0x27, 0xf2, + 0x11, 0xa3, 0xda, 0x03, 0xaa, 0x51, 0xc0, 0xfd, 0x90, 0xc8, 0x73, 0x81, 0xce, 0x97, 0x30, 0xa2, + 0x54, 0x63, 0x6f, 0xfc, 0x7f, 0x5b, 0x71, 0xec, 0x11, 0xb0, 0xa0, 0xc8, 0x74, 0x3a, 0xcc, 0x1b, + 0x5e, 0xcd, 0x91, 0xa8, 0x18, 0x92, 0xeb, 0x33, 0xc4, 0x6d, 0xb8, 0x16, 0x67, 0xe1, 0xc5, 0xa6, + 0x26, 0x35, 0x48, 0xc4, 0xe7, 0x94, 0xeb, 0xbb, 0xb8, 0xde, 0xd3, 0xe1, 0xc0, 0xcb, 0x00, 0x20, + 0xf6, 0xbc, 0xa9, 0xc5, 0x70, 0xc4, 0xda, 0x1b, 0x61, 0x0b, 0x9f, 0x0b, 0x19, 0x93, 0xaf, 0x8f, + 0x40, 0xbb, 0x26, 0x79, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x82, 0x01, 0x1d, 0x30, 0x82, 0x01, + 0x19, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, 0xa3, 0xda, 0xe5, 0xef, + 0xc3, 0x1c, 0x7a, 0xcf, 0x34, 0x2b, 0xa2, 0x42, 0x2b, 0x77, 0xcb, 0x62, 0xfb, 0x4c, 0x28, 0x51, + 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0x9c, 0xe1, 0xad, + 0x8f, 0xd4, 0x86, 0xd2, 0x1c, 0x7e, 0x48, 0x32, 0xf2, 0x28, 0xfe, 0x87, 0x90, 0xe3, 0xb1, 0xc5, + 0x8e, 0x30, 0x4a, 0x06, 0x03, 0x55, 0x1d, 0x1f, 0x04, 0x43, 0x30, 0x41, 0x30, 0x3f, 0xa0, 0x3d, + 0xa0, 0x3b, 0x86, 0x39, 0x66, 0x69, 0x6c, 0x65, 0x3a, 0x2f, 0x2f, 0x2f, 0x2f, 0x52, 0x44, 0x32, + 0x38, 0x31, 0x38, 0x37, 0x38, 0x30, 0x45, 0x33, 0x45, 0x45, 0x43, 0x2f, 0x43, 0x65, 0x72, 0x74, + 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x2f, 0x50, 0x72, 0x6f, 0x64, 0x32, 0x4c, 0x53, 0x52, 0x41, + 0x73, 0x68, 0x61, 0x32, 0x52, 0x44, 0x53, 0x4c, 0x4d, 0x2e, 0x63, 0x72, 0x6c, 0x30, 0x64, 0x06, + 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01, 0x04, 0x58, 0x30, 0x56, 0x30, 0x54, 0x06, + 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02, 0x86, 0x48, 0x66, 0x69, 0x6c, 0x65, 0x3a, + 0x2f, 0x2f, 0x2f, 0x2f, 0x52, 0x44, 0x32, 0x38, 0x31, 0x38, 0x37, 0x38, 0x30, 0x45, 0x33, 0x45, + 0x45, 0x43, 0x2f, 0x43, 0x65, 0x72, 0x74, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x2f, 0x52, 0x44, + 0x32, 0x38, 0x31, 0x38, 0x37, 0x38, 0x30, 0x45, 0x33, 0x45, 0x45, 0x43, 0x5f, 0x50, 0x72, 0x6f, + 0x64, 0x32, 0x4c, 0x53, 0x52, 0x41, 0x73, 0x68, 0x61, 0x32, 0x52, 0x44, 0x53, 0x4c, 0x4d, 0x2e, + 0x63, 0x72, 0x74, 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, + 0x00, 0x30, 0x17, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x04, 0x0b, 0x16, + 0x09, 0x54, 0x4c, 0x53, 0x7e, 0x42, 0x41, 0x53, 0x49, 0x43, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x55, 0xd5, + 0x94, 0x3b, 0x06, 0xef, 0xf2, 0xb0, 0xf9, 0xd7, 0x36, 0x2a, 0x36, 0xe0, 0xf1, 0xd9, 0x18, 0xc1, + 0x89, 0x7e, 0xa2, 0xcf, 0x01, 0x6f, 0x22, 0x7b, 0x34, 0x81, 0xf0, 0x7a, 0x45, 0x11, 0x6e, 0x75, + 0x4b, 0x0b, 0xa8, 0xcd, 0x92, 0x57, 0x19, 0x80, 0xb7, 0x6e, 0x1a, 0x4d, 0x12, 0x65, 0x91, 0x56, + 0x38, 0x17, 0x22, 0xa2, 0x75, 0xae, 0xf9, 0x12, 0x75, 0x38, 0xf3, 0x19, 0x74, 0xea, 0x87, 0x46, + 0x1f, 0x98, 0x2c, 0x2f, 0xf9, 0xfc, 0xb4, 0xdc, 0x25, 0xa0, 0xd3, 0x34, 0x1b, 0xbc, 0x21, 0xbb, + 0x3d, 0x82, 0xad, 0x15, 0xc6, 0x3d, 0x02, 0x75, 0x33, 0x70, 0x25, 0x0a, 0x1a, 0xf7, 0x4c, 0xcb, + 0x84, 0xa3, 0xc1, 0x78, 0xe6, 0xf5, 0xa1, 0x44, 0x54, 0xc8, 0x34, 0xfd, 0xef, 0xbf, 0x86, 0x81, + 0x9d, 0x9a, 0x7e, 0xb6, 0xad, 0x71, 0x7e, 0xe4, 0xd9, 0x71, 0x6c, 0xb9, 0xe7, 0xf2, 0xd6, 0xd7, + 0xbb, 0x66, 0x5a, 0x30, 0xf5, 0x29, 0xae, 0x02, 0x39, 0x3d, 0xea, 0x7a, 0x79, 0x1b, 0x53, 0xc5, + 0xbe, 0x8d, 0xfb, 0xe2, 0xe4, 0x8e, 0xc2, 0x04, 0xb3, 0x0a, 0x94, 0x75, 0xa3, 0xbf, 0xd4, 0x87, + 0xd2, 0x74, 0x15, 0x05, 0x5e, 0xd5, 0x8f, 0x94, 0x23, 0x41, 0x13, 0x3f, 0xbd, 0xed, 0x21, 0x55, + 0x96, 0xe9, 0xc4, 0x93, 0x34, 0x7f, 0xaa, 0xea, 0xe7, 0xb1, 0x9a, 0xca, 0x25, 0x91, 0x18, 0xdf, + 0x28, 0x05, 0x8e, 0x53, 0xb3, 0x8c, 0x8d, 0xcc, 0xf3, 0xf4, 0x78, 0x76, 0x76, 0x7b, 0x82, 0xd6, + 0x75, 0x7a, 0x7d, 0xb3, 0x23, 0x2c, 0xc7, 0xbe, 0xa6, 0xb0, 0x50, 0x4d, 0x6c, 0xe2, 0x90, 0x85, + 0x97, 0x77, 0x0d, 0x2f, 0xf5, 0x7b, 0xb0, 0xc6, 0xad, 0xfa, 0x9a, 0x2c, 0xdf, 0xeb, 0x0d, 0x60, + 0xd3, 0x0e, 0xa8, 0x5c, 0x43, 0xab, 0x09, 0x85, 0xa3, 0xa9, 0x31, 0x66, 0xbd, 0xe4, + ], + vec![ + 0x30, 0x82, 0x04, 0x59, 0x30, 0x82, 0x03, 0x45, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x05, 0x01, + 0x00, 0x00, 0x00, 0x02, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1d, 0x05, 0x00, 0x30, + 0x11, 0x31, 0x0f, 0x30, 0x0d, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x06, 0x42, 0x65, 0x63, 0x6b, + 0x65, 0x72, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x39, 0x31, 0x30, 0x32, 0x36, 0x32, 0x33, 0x32, 0x36, + 0x34, 0x35, 0x5a, 0x17, 0x0d, 0x33, 0x38, 0x30, 0x31, 0x31, 0x39, 0x30, 0x33, 0x31, 0x34, 0x30, + 0x37, 0x5a, 0x30, 0x81, 0xa6, 0x31, 0x81, 0xa3, 0x30, 0x27, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1e, + 0x20, 0x00, 0x6e, 0x00, 0x63, 0x00, 0x61, 0x00, 0x63, 0x00, 0x6e, 0x00, 0x5f, 0x00, 0x69, 0x00, + 0x70, 0x00, 0x5f, 0x00, 0x74, 0x00, 0x63, 0x00, 0x70, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x32, 0x00, + 0x37, 0x30, 0x33, 0x06, 0x03, 0x55, 0x04, 0x07, 0x1e, 0x2c, 0x00, 0x6e, 0x00, 0x63, 0x00, 0x61, + 0x00, 0x63, 0x00, 0x6e, 0x00, 0x5f, 0x00, 0x69, 0x00, 0x70, 0x00, 0x5f, 0x00, 0x74, 0x00, 0x63, + 0x00, 0x70, 0x00, 0x3a, 0x00, 0x31, 0x00, 0x32, 0x00, 0x37, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x2e, + 0x00, 0x30, 0x00, 0x2e, 0x00, 0x31, 0x30, 0x43, 0x06, 0x03, 0x55, 0x04, 0x05, 0x1e, 0x3c, 0x00, + 0x31, 0x00, 0x42, 0x00, 0x63, 0x00, 0x4b, 0x00, 0x65, 0x00, 0x56, 0x00, 0x33, 0x00, 0x4d, 0x00, + 0x67, 0x00, 0x74, 0x00, 0x6a, 0x00, 0x55, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x32, 0x00, 0x50, 0x00, + 0x49, 0x00, 0x68, 0x00, 0x35, 0x00, 0x52, 0x00, 0x57, 0x00, 0x56, 0x00, 0x36, 0x00, 0x42, 0x00, + 0x58, 0x00, 0x48, 0x00, 0x77, 0x00, 0x3d, 0x00, 0x0d, 0x00, 0x0a, 0x30, 0x58, 0x30, 0x09, 0x06, + 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x0f, 0x05, 0x00, 0x03, 0x4b, 0x00, 0x30, 0x48, 0x02, 0x41, 0x00, + 0xab, 0xac, 0x87, 0x11, 0x83, 0xbf, 0xe9, 0x48, 0x25, 0x00, 0x2c, 0x33, 0x31, 0x5e, 0x3d, 0x78, + 0xc8, 0x5f, 0x82, 0xcb, 0x36, 0x41, 0xf5, 0xb4, 0x65, 0x15, 0xee, 0x04, 0x31, 0xae, 0xe2, 0x48, + 0x58, 0x99, 0x7f, 0x4f, 0x90, 0x1d, 0xf7, 0x7c, 0xd7, 0xf8, 0x47, 0x93, 0xa0, 0xca, 0x9c, 0xdf, + 0x91, 0xb0, 0x41, 0xe8, 0x05, 0x4b, 0xdc, 0x24, 0x5b, 0x72, 0xf7, 0x68, 0x91, 0x84, 0xfb, 0x19, + 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x82, 0x01, 0xf4, 0x30, 0x82, 0x01, 0xf0, 0x30, 0x14, 0x06, + 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x04, 0x01, 0x01, 0xff, 0x04, 0x04, 0x01, + 0x00, 0x05, 0x00, 0x30, 0x3c, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x02, + 0x01, 0x01, 0xff, 0x04, 0x2c, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, + 0x00, 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x20, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x70, + 0x00, 0x6f, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x00, + 0x00, 0x30, 0x81, 0xdd, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x05, 0x01, + 0x01, 0xff, 0x04, 0x81, 0xcc, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x22, 0x04, 0x00, 0x00, 0x1c, 0x00, 0x4a, 0x00, 0x66, 0x00, 0x4a, 0x00, 0xb0, 0x00, 0x03, + 0x00, 0x33, 0x00, 0x64, 0x00, 0x32, 0x00, 0x36, 0x00, 0x37, 0x00, 0x39, 0x00, 0x35, 0x00, 0x34, + 0x00, 0x2d, 0x00, 0x65, 0x00, 0x65, 0x00, 0x62, 0x00, 0x37, 0x00, 0x2d, 0x00, 0x31, 0x00, 0x31, + 0x00, 0x64, 0x00, 0x31, 0x00, 0x2d, 0x00, 0x62, 0x00, 0x39, 0x00, 0x34, 0x00, 0x65, 0x00, 0x2d, + 0x00, 0x30, 0x00, 0x30, 0x00, 0x63, 0x00, 0x30, 0x00, 0x34, 0x00, 0x66, 0x00, 0x61, 0x00, 0x33, + 0x00, 0x30, 0x00, 0x38, 0x00, 0x30, 0x00, 0x64, 0x00, 0x00, 0x00, 0x33, 0x00, 0x64, 0x00, 0x32, + 0x00, 0x36, 0x00, 0x37, 0x00, 0x39, 0x00, 0x35, 0x00, 0x34, 0x00, 0x2d, 0x00, 0x65, 0x00, 0x65, + 0x00, 0x62, 0x00, 0x37, 0x00, 0x2d, 0x00, 0x31, 0x00, 0x31, 0x00, 0x64, 0x00, 0x31, 0x00, 0x2d, + 0x00, 0x62, 0x00, 0x39, 0x00, 0x34, 0x00, 0x65, 0x00, 0x2d, 0x00, 0x30, 0x00, 0x30, 0x00, 0x63, + 0x00, 0x30, 0x00, 0x34, 0x00, 0x66, 0x00, 0x61, 0x00, 0x33, 0x00, 0x30, 0x00, 0x38, 0x00, 0x30, + 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x30, 0x81, 0x80, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x06, 0x01, + 0x01, 0xff, 0x04, 0x70, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x50, 0x00, 0x57, 0x00, + 0x49, 0x00, 0x4e, 0x00, 0x2d, 0x00, 0x34, 0x00, 0x4c, 0x00, 0x34, 0x00, 0x4c, 0x00, 0x36, 0x00, + 0x41, 0x00, 0x4d, 0x00, 0x42, 0x00, 0x43, 0x00, 0x53, 0x00, 0x51, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x30, 0x00, 0x34, 0x00, 0x32, 0x00, 0x39, 0x00, 0x2d, 0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, + 0x30, 0x00, 0x30, 0x00, 0x2d, 0x00, 0x33, 0x00, 0x34, 0x00, 0x39, 0x00, 0x37, 0x00, 0x32, 0x00, + 0x2d, 0x00, 0x41, 0x00, 0x54, 0x00, 0x33, 0x00, 0x35, 0x00, 0x33, 0x00, 0x00, 0x00, 0x57, 0x00, + 0x4f, 0x00, 0x52, 0x00, 0x4b, 0x00, 0x47, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x30, 0x37, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x01, 0x01, 0xff, 0x04, 0x2d, + 0x30, 0x2b, 0xa1, 0x22, 0xa4, 0x20, 0x57, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x2d, 0x00, 0x34, 0x00, + 0x4c, 0x00, 0x34, 0x00, 0x4c, 0x00, 0x36, 0x00, 0x41, 0x00, 0x4d, 0x00, 0x42, 0x00, 0x43, 0x00, + 0x53, 0x00, 0x51, 0x00, 0x00, 0x00, 0x82, 0x05, 0x01, 0x00, 0x00, 0x00, 0x02, 0x30, 0x09, 0x06, + 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1d, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x3e, 0xd3, 0xd5, + 0x61, 0x8a, 0x87, 0x7b, 0x98, 0x2c, 0x6d, 0x20, 0x38, 0x12, 0x08, 0xd8, 0xf7, 0x83, 0x08, 0xf8, + 0xe6, 0xb2, 0xe1, 0x21, 0xe1, 0x30, 0x61, 0x12, 0x19, 0xe8, 0xc1, 0x41, 0xaf, 0x59, 0x7c, 0x1e, + 0x3e, 0xc8, 0x40, 0x9e, 0x24, 0xe8, 0x8d, 0x0c, 0x41, 0xfd, 0xf8, 0x3e, 0xa1, 0xb3, 0xac, 0x56, + 0xac, 0x52, 0x91, 0x5a, 0xf8, 0xd0, 0x40, 0x8e, 0x13, 0x47, 0xa9, 0x8a, 0x0a, 0x62, 0x6d, 0x11, + 0x89, 0x20, 0x56, 0xe7, 0xd6, 0x5f, 0x12, 0x44, 0x94, 0xbf, 0x63, 0x99, 0xa3, 0x42, 0x40, 0xd5, + 0xc6, 0x8c, 0x1f, 0x4b, 0xf8, 0xaf, 0x83, 0x8e, 0xf6, 0x74, 0xb2, 0x0b, 0x55, 0x13, 0x4a, 0x76, + 0xed, 0x37, 0xd8, 0x3d, 0x13, 0xe7, 0xae, 0x43, 0x4c, 0x9a, 0x61, 0x6c, 0x7b, 0x1b, 0xd1, 0xaa, + 0x00, 0x97, 0xdf, 0x5b, 0x85, 0x9f, 0xc8, 0xee, 0x6c, 0xe5, 0xa2, 0x63, 0x76, 0xe4, 0x06, 0xd3, + 0x2a, 0xe0, 0x55, 0xe1, 0x92, 0x78, 0xed, 0x03, 0x7b, 0x7d, 0x1a, 0x6e, 0xc2, 0x56, 0xdc, 0xad, + 0x6e, 0xd7, 0xa9, 0xfe, 0xa7, 0xfd, 0x09, 0x0a, 0xa6, 0xd5, 0x8a, 0x99, 0xa4, 0x75, 0x89, 0xad, + 0x84, 0xc7, 0x09, 0xf7, 0x4c, 0x6e, 0xd0, 0xe2, 0x80, 0x17, 0x62, 0xfa, 0x86, 0xfe, 0x43, 0x51, + 0xf2, 0xb4, 0xf6, 0xef, 0x3b, 0xb3, 0x3d, 0x1f, 0xef, 0xa3, 0xcb, 0xa2, 0x57, 0x25, 0x7c, 0x02, + 0xf2, 0x27, 0x1c, 0x87, 0x70, 0x8e, 0x84, 0x20, 0xfe, 0x1d, 0x4a, 0xc4, 0x87, 0x24, 0x3b, 0xba, + 0xff, 0x34, 0x1a, 0xe2, 0xff, 0xa2, 0x43, 0x39, 0xd8, 0x19, 0x97, 0xf8, 0xf0, 0xf9, 0x73, 0xa6, + 0xb6, 0x55, 0x64, 0xa6, 0xca, 0xa3, 0x48, 0x22, 0xb7, 0x1a, 0x9b, 0x98, 0x1a, 0x8e, 0x2f, 0xaa, + 0xec, 0xc1, 0xfe, 0x25, 0x36, 0x2b, 0x70, 0x97, 0x8c, 0x5b, 0x62, 0x21, 0xc3, + ], + ], + }), + }), + scope_list: vec![Scope(String::from("microsoft.com"))], + }; + req.license_header.preamble_message_size = u16::try_from(req.size()).expect("can't panic"); + req.into() +}); + +#[test] +fn from_buffer_correctly_parses_client_new_license_request() { + assert_eq!(*CLIENT_NEW_LICENSE_REQUEST, decode(REQUEST_BUFFER.as_slice()).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_client_new_license_request() { + let serialized_request = encode_vec(&*CLIENT_NEW_LICENSE_REQUEST).unwrap(); + + assert_eq!(REQUEST_BUFFER.as_slice(), serialized_request.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_client_new_license_request() { + assert_eq!(REQUEST_BUFFER.len(), CLIENT_NEW_LICENSE_REQUEST.size()); +} + +#[test] +fn client_new_license_request_creates_correctly() { + match &*SERVER_LICENSE_REQUEST { + LicensePdu::ServerLicenseRequest(license_request) => { + let (client_new_license_request, encryption_data) = ClientNewLicenseRequest::from_server_license_request( + license_request, + CLIENT_RANDOM_BUFFER.as_ref(), + PREMASTER_SECRET_BUFFER.as_ref(), + CLIENT_USERNAME, + CLIENT_MACHINE_NAME, + ) + .unwrap(); + + assert_eq!(encryption_data.license_key, LICENSE_KEY_BUFFER.as_ref()); + assert_eq!( + Into::::into(client_new_license_request), + *CLIENT_NEW_LICENSE_REQUEST + ); + } + _ => panic!("Invalid license pdu"), + } +} + +#[test] +fn salted_hash_produces_result_correctly() { + let result = salted_hash( + &PREMASTER_SECRET_BUFFER, + &CLIENT_RANDOM_BUFFER, + &SERVER_RANDOM_BUFFER, + b"A", + ); + + assert_eq!(result, SALTED_HASH_BUFFER.as_ref()); +} + +#[test] +fn master_secret_generates_correctly() { + let result = compute_master_secret( + PREMASTER_SECRET_BUFFER.as_ref(), + CLIENT_RANDOM_BUFFER.as_ref(), + SERVER_RANDOM_BUFFER.as_ref(), + ); + + assert_eq!(result, MASTER_SECRET_BUFFER.as_ref()); +} + +#[test] +fn session_key_blob_generates_correctly() { + let result = compute_session_key_blob( + MASTER_SECRET_BUFFER.as_ref(), + CLIENT_RANDOM_BUFFER.as_ref(), + SERVER_RANDOM_BUFFER.as_ref(), + ); + + assert_eq!(result, SESSION_KEY_BLOB.as_ref()); +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/mod.rs b/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/mod.rs new file mode 100644 index 00000000..d00e068a --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/mod.rs @@ -0,0 +1,305 @@ +#[cfg(test)] +mod test; + +use std::io::Write as _; + +use byteorder::{LittleEndian, WriteBytesExt as _}; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use super::{ + BasicSecurityHeader, BasicSecurityHeaderFlags, BlobHeader, BlobType, LicenseEncryptionData, LicenseHeader, + PreambleFlags, PreambleType, PreambleVersion, ServerLicenseError, ServerPlatformChallenge, BLOB_LENGTH_SIZE, + BLOB_TYPE_SIZE, MAC_SIZE, PLATFORM_ID, PREAMBLE_SIZE, +}; +use crate::crypto::rc4::Rc4; + +const RESPONSE_DATA_VERSION: u16 = 0x100; +const RESPONSE_DATA_STATIC_FIELDS_SIZE: usize = 8; + +pub(crate) const CLIENT_HARDWARE_IDENTIFICATION_SIZE: usize = 20; + +/// [2.2.2.5] Client Platform Challenge Response (CLIENT_PLATFORM_CHALLENGE_RESPONSE) +/// +/// [2.2.2.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/f53ab87c-d07d-4bf9-a2ac-79542f7b456c +#[derive(Debug, PartialEq, Eq)] +pub struct ClientPlatformChallengeResponse { + pub license_header: LicenseHeader, + pub encrypted_challenge_response_data: Vec, + pub encrypted_hwid: Vec, + pub mac_data: Vec, +} + +impl ClientPlatformChallengeResponse { + const NAME: &'static str = "ClientPlatformChallengeResponse"; + + pub fn from_server_platform_challenge( + platform_challenge: &ServerPlatformChallenge, + hardware_data: [u32; 4], + encryption_data: &LicenseEncryptionData, + ) -> Result { + let mut rc4 = Rc4::new(&encryption_data.license_key); + let decrypted_challenge = rc4.process(platform_challenge.encrypted_platform_challenge.as_slice()); + + let decrypted_challenge_mac = + super::compute_mac_data(encryption_data.mac_salt_key.as_slice(), decrypted_challenge.as_slice())?; + + if decrypted_challenge_mac != platform_challenge.mac_data { + return Err(ServerLicenseError::InvalidMacData); + } + + let mut challenge_response_data = vec![0u8; RESPONSE_DATA_STATIC_FIELDS_SIZE]; + challenge_response_data.write_u16::(RESPONSE_DATA_VERSION)?; + challenge_response_data.write_u16::(ClientType::Other.as_u16())?; + challenge_response_data.write_u16::(LicenseDetailLevel::Detail.as_u16())?; + challenge_response_data.write_u16::( + u16::try_from(decrypted_challenge.len()) + .map_err(|_| ServerLicenseError::InvalidField("decrypted challenge len"))?, + )?; + challenge_response_data.write_all(&decrypted_challenge)?; + + let mut hardware_id = Vec::with_capacity(CLIENT_HARDWARE_IDENTIFICATION_SIZE); + hardware_id.write_u32::(PLATFORM_ID)?; + for data in hardware_data { + hardware_id.write_u32::(data)?; + } + + let mut rc4 = Rc4::new(&encryption_data.license_key); + let encrypted_hwid = rc4.process(&hardware_id); + + let mut rc4 = Rc4::new(&encryption_data.license_key); + let encrypted_challenge_response_data = rc4.process(&challenge_response_data); + + challenge_response_data.extend(&hardware_id); + let mac_data = super::compute_mac_data( + encryption_data.mac_salt_key.as_slice(), + challenge_response_data.as_slice(), + )?; + + let license_header = LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::PlatformChallengeResponse, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: u16::try_from( + PREAMBLE_SIZE + + (BLOB_TYPE_SIZE + BLOB_LENGTH_SIZE) * 2 // 2 blobs in this structure + + MAC_SIZE + encrypted_challenge_response_data.len() + encrypted_hwid.len(), + ) + .map_err(|_| ServerLicenseError::InvalidField("preamble message size"))?, + }; + + Ok(Self { + license_header, + encrypted_challenge_response_data, + encrypted_hwid, + mac_data, + }) + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.license_header.encode(dst)?; + + BlobHeader::new(BlobType::ENCRYPTED_DATA, self.encrypted_challenge_response_data.len()).encode(dst)?; + dst.write_slice(&self.encrypted_challenge_response_data); + + BlobHeader::new(BlobType::ENCRYPTED_DATA, self.encrypted_hwid.len()).encode(dst)?; + dst.write_slice(&self.encrypted_hwid); + + dst.write_slice(&self.mac_data); + + Ok(()) + } + + pub fn decode(license_header: LicenseHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + if license_header.preamble_message_type != PreambleType::PlatformChallengeResponse { + return Err(invalid_field_err!( + "preambleMessageType", + "unexpected preamble message type" + )); + } + + let encrypted_challenge_blob = BlobHeader::decode(src)?; + if encrypted_challenge_blob.blob_type != BlobType::ENCRYPTED_DATA { + return Err(invalid_field_err!("blobType", "unexpected blob type")); + } + ensure_size!(in: src, size: encrypted_challenge_blob.length); + let encrypted_challenge_response_data = src.read_slice(encrypted_challenge_blob.length).into(); + + let encrypted_hwid_blob = BlobHeader::decode(src)?; + if encrypted_hwid_blob.blob_type != BlobType::ENCRYPTED_DATA { + return Err(invalid_field_err!("blobType", "unexpected blob type")); + } + ensure_size!(in: src, size: encrypted_hwid_blob.length); + let encrypted_hwid = src.read_slice(encrypted_hwid_blob.length).into(); + + ensure_size!(in: src, size: MAC_SIZE); + let mac_data = src.read_slice(MAC_SIZE).into(); + + Ok(Self { + license_header, + encrypted_challenge_response_data, + encrypted_hwid, + mac_data, + }) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + self.license_header.size() + + (BLOB_TYPE_SIZE + BLOB_LENGTH_SIZE) * 2 // 2 blobs in this structure + + MAC_SIZE + self.encrypted_challenge_response_data.len() + self.encrypted_hwid.len() + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive)] +pub enum ClientType { + Win32 = 0x0100, + Win16 = 0x0200, + WinCe = 0x0300, + Other = 0xff00, +} + +impl ClientType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive)] +pub enum LicenseDetailLevel { + Simple = 1, + Moderate = 2, + Detail = 3, +} + +impl LicenseDetailLevel { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[derive(Debug, PartialEq)] +pub struct PlatformChallengeResponseData { + pub client_type: ClientType, + pub license_detail_level: LicenseDetailLevel, + pub challenge: Vec, +} + +impl PlatformChallengeResponseData { + const NAME: &'static str = "PlatformChallengeResponseData"; + + const FIXED_PART_SIZE: usize = RESPONSE_DATA_STATIC_FIELDS_SIZE; +} + +impl Encode for PlatformChallengeResponseData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(RESPONSE_DATA_VERSION); + dst.write_u16(self.client_type.as_u16()); + dst.write_u16(self.license_detail_level.as_u16()); + dst.write_u16(cast_length!("len", self.challenge.len())?); + dst.write_slice(&self.challenge); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.challenge.len() + } +} + +impl<'de> Decode<'de> for PlatformChallengeResponseData { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version = src.read_u16(); + if version != RESPONSE_DATA_VERSION { + return Err(invalid_field_err!("version", "invalid challenge response version")); + } + + let client_type = ClientType::from_u16(src.read_u16()) + .ok_or_else(|| invalid_field_err!("clientType", "invalid client type"))?; + + let license_detail_level = LicenseDetailLevel::from_u16(src.read_u16()) + .ok_or_else(|| invalid_field_err!("licenseDetailLevel", "invalid license detail level"))?; + + let challenge_len: usize = cast_length!("len", src.read_u16())?; + ensure_size!(in: src, size: challenge_len); + let challenge = src.read_slice(challenge_len).into(); + + Ok(Self { + client_type, + license_detail_level, + challenge, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ClientHardwareIdentification { + pub platform_id: u32, + pub data: Vec, +} + +impl ClientHardwareIdentification { + const NAME: &'static str = "ClientHardwareIdentification"; + + const FIXED_PART_SIZE: usize = CLIENT_HARDWARE_IDENTIFICATION_SIZE; +} + +impl Encode for ClientHardwareIdentification { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.platform_id); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ClientHardwareIdentification { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let platform_id = src.read_u32(); + let data = src.read_slice(MAC_SIZE).into(); + + Ok(Self { platform_id, data }) + } +} diff --git a/ironrdp/src/rdp/server_license/client_platform_challenge_response/test.rs b/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/test.rs similarity index 54% rename from ironrdp/src/rdp/server_license/client_platform_challenge_response/test.rs rename to crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/test.rs index 2a08275b..d086f954 100644 --- a/ironrdp/src/rdp/server_license/client_platform_challenge_response/test.rs +++ b/crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/test.rs @@ -1,10 +1,9 @@ -use super::*; -use crate::rdp::server_license::{ - BasicSecurityHeader, BasicSecurityHeaderFlags, LicenseHeader, PreambleFlags, PreambleType, - PreambleVersion, BASIC_SECURITY_HEADER_SIZE, PREAMBLE_SIZE, -}; +use std::sync::LazyLock; -use lazy_static::lazy_static; +use ironrdp_core::{decode, encode_vec}; + +use super::*; +use crate::rdp::server_license::{LicensePdu, BASIC_SECURITY_HEADER_SIZE}; const PLATFORM_CHALLENGE_RESPONSE_DATA_BUFFER: [u8; 18] = [ 0x00, 0x01, // version @@ -28,14 +27,13 @@ const CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER: [u8; 70] = [ 0x15, 0x03, 0x42, 0x00, // preamble 0x09, 0x00, // blob type, ignored 0x12, 0x00, // blob len - 0xfa, 0xb4, 0xe8, 0x24, 0xcf, 0x56, 0xb2, 0x4e, 0x80, 0x02, 0xbd, 0xb6, 0x61, 0xfc, 0xdf, 0xe9, - 0x6c, 0x44, // encrypted platform challenge response + 0xfa, 0xb4, 0xe8, 0x24, 0xcf, 0x56, 0xb2, 0x4e, 0x80, 0x02, 0xbd, 0xb6, 0x61, 0xfc, 0xdf, 0xe9, 0x6c, + 0x44, // encrypted platform challenge response 0x09, 0x00, // blob type, ignored 0x14, 0x00, // blob len - 0xf8, 0xb5, 0xe8, 0x25, 0x3d, 0x0f, 0x3f, 0x70, 0x1d, 0xda, 0x60, 0x19, 0x16, 0xfe, 0x73, 0x1a, - 0x45, 0x7e, 0x02, 0x71, // encrypted hwid - 0x38, 0x23, 0x62, 0x5d, 0x10, 0x8b, 0x93, 0xc3, 0xf1, 0xe4, 0x67, 0x1f, 0x4a, 0xb6, 0x00, - 0x0a, // mac data + 0xf8, 0xb5, 0xe8, 0x25, 0x3d, 0x0f, 0x3f, 0x70, 0x1d, 0xda, 0x60, 0x19, 0x16, 0xfe, 0x73, 0x1a, 0x45, 0x7e, 0x02, + 0x71, // encrypted hwid + 0x38, 0x23, 0x62, 0x5d, 0x10, 0x8b, 0x93, 0xc3, 0xf1, 0xe4, 0x67, 0x1f, 0x4a, 0xb6, 0x00, 0x0a, // mac data ]; const CHALLENGE_BUFFER: [u8; 10] = [ @@ -47,55 +45,49 @@ const DATA_BUFFER: [u8; 16] = [ 0xf1, 0x59, 0x87, 0x3e, 0xc9, 0xd8, 0x98, 0xaf, 0x24, 0x02, 0xf8, 0xf3, 0x29, 0x3a, 0xf0, 0x26, ]; -lazy_static! { - pub static ref RESPONSE: PlatformChallengeResponseData = PlatformChallengeResponseData { - client_type: ClientType::Win32, - license_detail_level: LicenseDetailLevel::Detail, - challenge: Vec::from(CHALLENGE_BUFFER.as_ref()), - }; - pub static ref CLIENT_HARDWARE_IDENTIFICATION: ClientHardwareIdentification = - ClientHardwareIdentification { - platform_id: HARDWARE_ID, - data: Vec::from(DATA_BUFFER.as_ref()), - }; - pub static ref CLIENT_PLATFORM_CHALLENGE_RESPONSE: ClientPlatformChallengeResponse = - ClientPlatformChallengeResponse { - license_header: LicenseHeader { - security_header: BasicSecurityHeader { - flags: BasicSecurityHeaderFlags::LICENSE_PKT, - }, - preamble_message_type: PreambleType::PlatformChallengeResponse, - preamble_flags: PreambleFlags::empty(), - preamble_version: PreambleVersion::V3, - preamble_message_size: (CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.len() - - BASIC_SECURITY_HEADER_SIZE) as u16, +pub(crate) static RESPONSE: LazyLock = LazyLock::new(|| PlatformChallengeResponseData { + client_type: ClientType::Win32, + license_detail_level: LicenseDetailLevel::Detail, + challenge: Vec::from(CHALLENGE_BUFFER.as_ref()), +}); +pub(crate) static CLIENT_HARDWARE_IDENTIFICATION: LazyLock = + LazyLock::new(|| ClientHardwareIdentification { + platform_id: HARDWARE_ID, + data: Vec::from(DATA_BUFFER.as_ref()), + }); +pub(crate) static CLIENT_PLATFORM_CHALLENGE_RESPONSE: LazyLock = LazyLock::new(|| { + LicensePdu::ClientPlatformChallengeResponse(ClientPlatformChallengeResponse { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, }, - encrypted_challenge_response_data: Vec::from( - &CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER[12..30] - ), - encrypted_hwid: Vec::from(&CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER[34..54]), - mac_data: Vec::from( - &CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER - [CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.len() - 16..] - ), - }; -} + preamble_message_type: PreambleType::PlatformChallengeResponse, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: u16::try_from( + CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE, + ) + .expect("can't panic"), + }, + encrypted_challenge_response_data: Vec::from(&CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER[12..30]), + encrypted_hwid: Vec::from(&CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER[34..54]), + mac_data: Vec::from( + &CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER[CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.len() - 16..], + ), + }) +}); #[test] fn from_buffer_correctly_parses_platform_challenge_response_data() { assert_eq!( *RESPONSE, - PlatformChallengeResponseData::from_buffer( - PLATFORM_CHALLENGE_RESPONSE_DATA_BUFFER.as_ref() - ) - .unwrap() + decode(PLATFORM_CHALLENGE_RESPONSE_DATA_BUFFER.as_ref()).unwrap() ); } #[test] fn to_buffer_correctly_serializes_platform_challenge_response_data() { - let mut serialized_response = Vec::new(); - RESPONSE.to_buffer(&mut serialized_response).unwrap(); + let serialized_response = encode_vec(&*RESPONSE).unwrap(); assert_eq!( PLATFORM_CHALLENGE_RESPONSE_DATA_BUFFER.as_ref(), @@ -105,27 +97,20 @@ fn to_buffer_correctly_serializes_platform_challenge_response_data() { #[test] fn buffer_length_is_correct_for_platform_challenge_response_data() { - assert_eq!( - PLATFORM_CHALLENGE_RESPONSE_DATA_BUFFER.len(), - RESPONSE.buffer_length() - ); + assert_eq!(PLATFORM_CHALLENGE_RESPONSE_DATA_BUFFER.len(), RESPONSE.size()); } #[test] fn from_buffer_correctly_parses_client_hardware_identification() { assert_eq!( *CLIENT_HARDWARE_IDENTIFICATION, - ClientHardwareIdentification::from_buffer(CLIENT_HARDWARE_IDENTIFICATION_BUFFER.as_ref()) - .unwrap() + decode(CLIENT_HARDWARE_IDENTIFICATION_BUFFER.as_ref()).unwrap() ); } #[test] fn to_buffer_correctly_serializes_client_hardware_identification() { - let mut serialized_hardware_identification = Vec::new(); - CLIENT_HARDWARE_IDENTIFICATION - .to_buffer(&mut serialized_hardware_identification) - .unwrap(); + let serialized_hardware_identification = encode_vec(&*CLIENT_HARDWARE_IDENTIFICATION).unwrap(); assert_eq!( CLIENT_HARDWARE_IDENTIFICATION_BUFFER.as_ref(), @@ -137,7 +122,7 @@ fn to_buffer_correctly_serializes_client_hardware_identification() { fn buffer_length_is_correct_for_client_hardware_identification() { assert_eq!( CLIENT_HARDWARE_IDENTIFICATION_BUFFER.len(), - CLIENT_HARDWARE_IDENTIFICATION.buffer_length() + CLIENT_HARDWARE_IDENTIFICATION.size() ); } @@ -145,19 +130,13 @@ fn buffer_length_is_correct_for_client_hardware_identification() { fn from_buffer_correctly_parses_client_platform_challenge_response() { assert_eq!( *CLIENT_PLATFORM_CHALLENGE_RESPONSE, - ClientPlatformChallengeResponse::from_buffer( - CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.as_ref() - ) - .unwrap() + decode(CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.as_ref()).unwrap() ); } #[test] fn to_buffer_correctly_serializes_client_platform_challenge_response() { - let mut serialized_response = Vec::new(); - CLIENT_PLATFORM_CHALLENGE_RESPONSE - .to_buffer(&mut serialized_response) - .unwrap(); + let serialized_response = encode_vec(&*CLIENT_PLATFORM_CHALLENGE_RESPONSE).unwrap(); assert_eq!( CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.as_ref(), @@ -166,21 +145,19 @@ fn to_buffer_correctly_serializes_client_platform_challenge_response() { } #[test] -fn buffer_length_is_correct_for_client_platform_challege_response() { +fn buffer_length_is_correct_for_client_platform_challenge_response() { assert_eq!( CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.len(), - CLIENT_PLATFORM_CHALLENGE_RESPONSE.buffer_length() + CLIENT_PLATFORM_CHALLENGE_RESPONSE.size() ); } #[test] fn challenge_response_creates_from_server_challenge_and_encryption_data_correctly() { - let encrypted_platform_challenge = - vec![0x26, 0x38, 0x88, 0x77, 0xcb, 0xe8, 0xbf, 0xce, 0x2c, 0x51]; + let encrypted_platform_challenge = vec![0x26, 0x38, 0x88, 0x77, 0xcb, 0xe8, 0xbf, 0xce, 0x2c, 0x51]; let mac_data = vec![ - 0x51, 0x4a, 0x27, 0x2c, 0x74, 0x18, 0xec, 0x88, 0x95, 0xdd, 0xac, 0x10, 0x3e, 0x3f, 0xa, - 0x72, + 0x51, 0x4a, 0x27, 0x2c, 0x74, 0x18, 0xec, 0x88, 0x95, 0xdd, 0xac, 0x10, 0x3e, 0x3f, 0xa, 0x72, ]; let server_challenge = ServerPlatformChallenge { @@ -191,9 +168,8 @@ fn challenge_response_creates_from_server_challenge_and_encryption_data_correctl preamble_message_type: PreambleType::PlatformChallenge, preamble_flags: PreambleFlags::empty(), preamble_version: PreambleVersion::V3, - preamble_message_size: (encrypted_platform_challenge.len() - + mac_data.len() - + PREAMBLE_SIZE) as u16, + preamble_message_size: u16::try_from(encrypted_platform_challenge.len() + mac_data.len() + PREAMBLE_SIZE) + .expect("can't panic"), }, encrypted_platform_challenge, mac_data, @@ -202,29 +178,24 @@ fn challenge_response_creates_from_server_challenge_and_encryption_data_correctl let encryption_data = LicenseEncryptionData { premaster_secret: Vec::new(), // premaster secret is not involved in this unit test mac_salt_key: vec![ - 0x1, 0x5b, 0x9e, 0x5f, 0x6, 0x97, 0x71, 0x58, 0xc3, 0xb8, 0x8b, 0x8c, 0x6e, 0x77, 0x21, - 0x37, + 0x1, 0x5b, 0x9e, 0x5f, 0x6, 0x97, 0x71, 0x58, 0xc3, 0xb8, 0x8b, 0x8c, 0x6e, 0x77, 0x21, 0x37, ], license_key: vec![ - 0xe1, 0x78, 0xe4, 0xa0, 0x2a, 0xc5, 0xca, 0xb8, 0xa2, 0xd1, 0x53, 0xb8, 0x7, 0x23, - 0xf3, 0xd2, + 0xe1, 0x78, 0xe4, 0xa0, 0x2a, 0xc5, 0xca, 0xb8, 0xa2, 0xd1, 0x53, 0xb8, 0x7, 0x23, 0xf3, 0xd2, ], }; + let hardware_data = vec![0u8; 16]; let mut hardware_id = Vec::with_capacity(CLIENT_HARDWARE_IDENTIFICATION_SIZE); - let mut md5 = md5::Md5::new(); - md5.input(b"sample-hostname"); - let hardware_data = &md5.result(); - hardware_id.write_u32::(PLATFORM_ID).unwrap(); - hardware_id.write_all(hardware_data).unwrap(); + hardware_id.write_all(&hardware_data).unwrap(); let mut rc4 = Rc4::new(&encryption_data.license_key); let encrypted_hwid = rc4.process(&hardware_id); let response_data: [u8; 26] = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xff, 0x03, 0x00, 0x0a, - 0x00, 0x54, 0x00, 0x45, 0x00, 0x53, 0x00, 0x54, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xff, 0x03, 0x00, 0x0a, 0x00, 0x54, 0x00, + 0x45, 0x00, 0x53, 0x00, 0x54, 0x00, 0x00, 0x00, ]; let mut rc4 = Rc4::new(&encryption_data.license_key); @@ -232,10 +203,9 @@ fn challenge_response_creates_from_server_challenge_and_encryption_data_correctl let mac_data = crate::rdp::server_license::compute_mac_data( encryption_data.mac_salt_key.as_slice(), - [response_data.as_ref(), hardware_id.as_slice()] - .concat() - .as_slice(), - ); + [response_data.as_ref(), hardware_id.as_slice()].concat().as_slice(), + ) + .expect("can't panic"); let correct_challenge_response = ClientPlatformChallengeResponse { license_header: LicenseHeader { @@ -245,22 +215,21 @@ fn challenge_response_creates_from_server_challenge_and_encryption_data_correctl preamble_message_type: PreambleType::PlatformChallengeResponse, preamble_flags: PreambleFlags::empty(), preamble_version: PreambleVersion::V3, - preamble_message_size: (PREAMBLE_SIZE + preamble_message_size: u16::try_from( + PREAMBLE_SIZE + (BLOB_TYPE_SIZE + BLOB_LENGTH_SIZE) * 2 // 2 blobs in this structure - + MAC_SIZE + encrypted_challenge_response_data.len() + encrypted_hwid.len()) - as u16, + + MAC_SIZE + encrypted_challenge_response_data.len() + encrypted_hwid.len(), + ) + .expect("can't panic"), }, encrypted_challenge_response_data, encrypted_hwid, mac_data, }; - let challenge_response = ClientPlatformChallengeResponse::from_server_platform_challenge( - &server_challenge, - "sample-hostname", - &encryption_data, - ) - .unwrap(); + let challenge_response = + ClientPlatformChallengeResponse::from_server_platform_challenge(&server_challenge, [0u32; 4], &encryption_data) + .unwrap(); assert_eq!(challenge_response, correct_challenge_response); } diff --git a/crates/ironrdp-pdu/src/rdp/server_license/licensing_error_message/mod.rs b/crates/ironrdp-pdu/src/rdp/server_license/licensing_error_message/mod.rs new file mode 100644 index 00000000..dde01eb3 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/licensing_error_message/mod.rs @@ -0,0 +1,147 @@ +#[cfg(test)] +mod test; + +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode as _, DecodeResult, Encode as _, + EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use super::{BlobHeader, BlobType, LicenseHeader, PreambleFlags, PreambleVersion, BLOB_LENGTH_SIZE, BLOB_TYPE_SIZE}; +use crate::rdp::headers::{BasicSecurityHeader, BasicSecurityHeaderFlags, BASIC_SECURITY_HEADER_SIZE}; +use crate::rdp::server_license::PreambleType; + +const ERROR_CODE_SIZE: usize = 4; +const STATE_TRANSITION_SIZE: usize = 4; + +/// [2.2.1.12.1.3] Licensing Error Message (LICENSE_ERROR_MESSAGE) +/// +/// [2.2.1.12.1.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/f18b6c9f-f3d8-4a0e-8398-f9b153233dca +#[derive(Debug, PartialEq, Eq)] +pub struct LicensingErrorMessage { + pub license_header: LicenseHeader, + pub error_code: LicenseErrorCode, + pub state_transition: LicensingStateTransition, + pub error_info: Vec, +} + +impl LicensingErrorMessage { + const NAME: &'static str = "LicensingErrorMessage"; + + const FIXED_PART_SIZE: usize = ERROR_CODE_SIZE + STATE_TRANSITION_SIZE; + + pub fn new_valid_client() -> EncodeResult { + let mut this = Self { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::ErrorAlert, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: 0, + }, + error_code: LicenseErrorCode::StatusValidClient, + state_transition: LicensingStateTransition::NoTransition, + error_info: Vec::new(), + }; + this.license_header.preamble_message_size = cast_length!( + "LicensingErrorMessage", + "preamble_message_size", + this.size() - BASIC_SECURITY_HEADER_SIZE + )?; + Ok(this) + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.license_header.encode(dst)?; + + dst.write_u32(self.error_code.as_u32()); + dst.write_u32(self.state_transition.as_u32()); + + BlobHeader::new(BlobType::ERROR, self.error_info.len()).encode(dst)?; + dst.write_slice(&self.error_info); + + Ok(()) + } + + pub fn decode(license_header: LicenseHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + if license_header.preamble_message_type != PreambleType::ErrorAlert { + return Err(invalid_field_err!("preambleMessageType", "unexpected preamble type")); + } + + ensure_fixed_part_size!(in: src); + let error_code = LicenseErrorCode::from_u32(src.read_u32()) + .ok_or_else(|| invalid_field_err!("errorCode", "invalid error code"))?; + let state_transition = LicensingStateTransition::from_u32(src.read_u32()) + .ok_or_else(|| invalid_field_err!("stateTransition", "invalid state transition"))?; + + let error_info_blob = BlobHeader::decode(src)?; + if error_info_blob.length != 0 && error_info_blob.blob_type != BlobType::ERROR { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + + let error_info = vec![0u8; error_info_blob.length]; + + Ok(Self { + license_header, + error_code, + state_transition, + error_info, + }) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + self.license_header.size() + Self::FIXED_PART_SIZE + self.error_info.len() + BLOB_LENGTH_SIZE + BLOB_TYPE_SIZE + } +} + +#[repr(u32)] +#[derive(Debug, PartialEq, Eq, FromPrimitive, Copy, Clone)] +pub enum LicenseErrorCode { + InvalidServerCertificate = 0x01, + NoLicense = 0x02, + InvalidMac = 0x03, + InvalidScope = 0x4, + NoLicenseServer = 0x06, + StatusValidClient = 0x07, + InvalidClient = 0x08, + InvalidProductId = 0x0b, + InvalidFieldLen = 0x0c, +} + +impl LicenseErrorCode { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[repr(u32)] +#[derive(Debug, PartialEq, Eq, FromPrimitive, Copy, Clone)] +pub enum LicensingStateTransition { + TotalAbort = 1, + NoTransition = 2, + ResetPhaseToStart = 3, + ResendLastMessage = 4, +} + +impl LicensingStateTransition { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/licensing_error_message/test.rs b/crates/ironrdp-pdu/src/rdp/server_license/licensing_error_message/test.rs new file mode 100644 index 00000000..7ccfc467 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/licensing_error_message/test.rs @@ -0,0 +1,57 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; +use crate::rdp::server_license::LicensePdu; + +const HEADER_MESSAGE_BUFFER: [u8; 8] = [0x80, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x14, 0x00]; + +const LICENSE_MESSAGE_BUFFER: [u8; 12] = [ + 0x07, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, // message +]; + +static LICENSING_ERROR_MESSAGE: LazyLock = LazyLock::new(|| { + let mut pdu = LicensingErrorMessage { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::ErrorAlert, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: 0, + }, + error_code: LicenseErrorCode::StatusValidClient, + state_transition: LicensingStateTransition::NoTransition, + error_info: Vec::new(), + }; + pdu.license_header.preamble_message_size = u16::try_from(pdu.size()).expect("can't panic"); + pdu.into() +}); + +#[test] +fn from_buffer_correctly_parses_licensing_error_message() { + assert_eq!( + *LICENSING_ERROR_MESSAGE, + decode(&[&HEADER_MESSAGE_BUFFER[..], &LICENSE_MESSAGE_BUFFER[..]].concat()).unwrap(), + ); +} + +#[test] +fn to_buffer_correctly_serializes_licensing_error_message() { + let buffer = encode_vec(&*LICENSING_ERROR_MESSAGE).unwrap(); + + assert_eq!( + [&HEADER_MESSAGE_BUFFER[..], &LICENSE_MESSAGE_BUFFER[..]].concat(), + buffer + ); +} + +#[test] +fn buffer_length_is_correct_for_licensing_error_message() { + assert_eq!( + HEADER_MESSAGE_BUFFER.len() + LICENSE_MESSAGE_BUFFER.len(), + LICENSING_ERROR_MESSAGE.size() + ); +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/mod.rs b/crates/ironrdp-pdu/src/rdp/server_license/mod.rs new file mode 100644 index 00000000..170ce33c --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/mod.rs @@ -0,0 +1,482 @@ +use std::io; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, invalid_field_err, unsupported_value_err, Decode, DecodeResult, Encode, + EncodeResult, ReadCursor, WriteCursor, +}; +use md5::Digest as _; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; +use thiserror::Error; + +use crate::rdp::headers::{BasicSecurityHeader, BasicSecurityHeaderFlags, BASIC_SECURITY_HEADER_SIZE}; +pub use crate::rdp::server_license::client_license_info::ClientLicenseInfo; +use crate::PduError; + +#[cfg(test)] +mod tests; + +mod client_license_info; +mod client_new_license_request; +mod client_platform_challenge_response; +mod licensing_error_message; +mod server_license_request; +mod server_platform_challenge; +mod server_upgrade_license; + +pub use self::client_new_license_request::{ClientNewLicenseRequest, PLATFORM_ID}; +pub use self::client_platform_challenge_response::{ + ClientHardwareIdentification, ClientPlatformChallengeResponse, PlatformChallengeResponseData, +}; +pub use self::licensing_error_message::{LicenseErrorCode, LicensingErrorMessage, LicensingStateTransition}; +pub use self::server_license_request::{cert, ProductInfo, Scope, ServerCertificate, ServerLicenseRequest}; +pub use self::server_platform_challenge::ServerPlatformChallenge; +pub use self::server_upgrade_license::{LicenseInformation, ServerUpgradeLicense}; + +pub const PREAMBLE_SIZE: usize = 4; +pub const PREMASTER_SECRET_SIZE: usize = 48; +pub const RANDOM_NUMBER_SIZE: usize = 32; + +const PROTOCOL_VERSION_MASK: u8 = 0x0F; + +const BLOB_TYPE_SIZE: usize = 2; +const BLOB_LENGTH_SIZE: usize = 2; + +const UTF8_NULL_TERMINATOR_SIZE: usize = 1; +const UTF16_NULL_TERMINATOR_SIZE: usize = 2; + +const KEY_EXCHANGE_ALGORITHM_RSA: u32 = 1; + +const MAC_SIZE: usize = 16; + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct LicenseEncryptionData { + pub premaster_secret: Vec, + pub mac_salt_key: Vec, + pub license_key: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct LicenseHeader { + pub security_header: BasicSecurityHeader, + pub preamble_message_type: PreambleType, + pub preamble_flags: PreambleFlags, + pub preamble_version: PreambleVersion, + pub preamble_message_size: u16, +} + +impl LicenseHeader { + const NAME: &'static str = "LicenseHeader"; + + const FIXED_PART_SIZE: usize = PREAMBLE_SIZE + BASIC_SECURITY_HEADER_SIZE; +} + +impl Encode for LicenseHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + self.security_header.encode(dst)?; + + let flags_with_version = self.preamble_flags.bits() | self.preamble_version.as_u8(); + + dst.write_u8(self.preamble_message_type.as_u8()); + dst.write_u8(flags_with_version); + dst.write_u16(self.preamble_message_size); // msg size + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for LicenseHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let security_header = BasicSecurityHeader::decode(src)?; + + if !security_header.flags.contains(BasicSecurityHeaderFlags::LICENSE_PKT) { + return Err(invalid_field_err!( + "securityHeaderFlags", + "invalid security header flags" + )); + } + + let preamble_message_type = PreambleType::from_u8(src.read_u8()) + .ok_or_else(|| invalid_field_err!("preambleType", "invalid license type"))?; + + let flags_with_version = src.read_u8(); + let preamble_message_size = src.read_u16(); + + let preamble_flags = PreambleFlags::from_bits(flags_with_version & !PROTOCOL_VERSION_MASK) + .ok_or_else(|| invalid_field_err!("preambleFlags", "Got invalid flags field"))?; + + let preamble_version = PreambleVersion::from_u8(flags_with_version & PROTOCOL_VERSION_MASK) + .ok_or_else(|| invalid_field_err!("preambleVersion", "Got invalid version in the flags filed"))?; + + Ok(Self { + security_header, + preamble_message_type, + preamble_flags, + preamble_version, + preamble_message_size, + }) + } +} + +/// [2.2.1.12.1.1] Licensing Preamble (LICENSE_PREAMBLE) +/// +/// [2.2.1.12.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/73170ca2-5f82-4a2d-9d1b-b439f3d8dadc +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, FromPrimitive, Copy, Clone)] +pub enum PreambleType { + LicenseRequest = 0x01, + PlatformChallenge = 0x02, + NewLicense = 0x03, + UpgradeLicense = 0x04, + LicenseInfo = 0x12, + NewLicenseRequest = 0x13, + PlatformChallengeResponse = 0x15, + ErrorAlert = 0xff, +} + +impl PreambleType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct PreambleFlags: u8 { + const EXTENDED_ERROR_MSG_SUPPORTED = 0x80; + } +} + +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, FromPrimitive, Copy, Clone)] +pub enum PreambleVersion { + V2 = 2, // RDP 4.0 + V3 = 3, // RDP 5.0, 5.1, 5.2, 6.0, 6.1, 7.0, 7.1, 8.0, 8.1, 10.0, 10.1, 10.2, 10.3, 10.4, and 10.5 +} + +impl PreambleVersion { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BlobType(u16); + +impl BlobType { + pub const ANY: Self = Self(0x00); + pub const DATA: Self = Self(0x01); + pub const RANDOM: Self = Self(0x02); + pub const CERTIFICATE: Self = Self(0x03); + pub const ERROR: Self = Self(0x04); + pub const RSA_KEY: Self = Self(0x06); + pub const ENCRYPTED_DATA: Self = Self(0x09); + pub const RSA_SIGNATURE: Self = Self(0x08); + pub const KEY_EXCHANGE_ALGORITHM: Self = Self(0x0d); + pub const SCOPE: Self = Self(0x0e); + pub const CLIENT_USER_NAME: Self = Self(0x0f); + pub const CLIENT_MACHINE_NAME_BLOB: Self = Self(0x10); +} + +#[derive(Debug, Error)] +pub enum ServerLicenseError { + #[error("IO error: {0}")] + IOError(#[from] io::Error), + #[error("UTF-8 error: {0}")] + Utf8Error(#[from] std::string::FromUtf8Error), + #[error("DER error: {0}")] + DerError(#[from] pkcs1::der::Error), + #[error("invalid `{0}`: out of range integral type conversion")] + InvalidField(&'static str), + #[error("invalid preamble field: {0}")] + InvalidPreamble(String), + #[error("invalid preamble message type field")] + InvalidLicenseType, + #[error("invalid error code field")] + InvalidErrorCode, + #[error("invalid state transition field")] + InvalidStateTransition, + #[error("invalid blob type field")] + InvalidBlobType, + #[error("unable to generate random number {0}")] + RandomNumberGenerationError(String), + #[error("unable to retrieve public key from the certificate")] + UnableToGetPublicKey, + #[error("unable to encrypt RSA public key")] + RsaKeyEncryptionError, + #[error("invalid License Request key exchange algorithm value")] + InvalidKeyExchangeValue, + #[error("MAC checksum generated over decrypted data does not match the server's checksum")] + InvalidMacData, + #[error("invalid platform challenge response data version")] + InvalidChallengeResponseDataVersion, + #[error("invalid platform challenge response data client type")] + InvalidChallengeResponseDataClientType, + #[error("invalid platform challenge response data license detail level")] + InvalidChallengeResponseDataLicenseDetail, + #[error("invalid x509 certificate")] + InvalidX509Certificate { + source: x509_cert::der::Error, + cert_der: Vec, + }, + #[error("invalid certificate version")] + InvalidCertificateVersion, + #[error("invalid x509 certificates amount")] + InvalidX509CertificatesAmount, + #[error("invalid proprietary certificate signature algorithm ID")] + InvalidPropCertSignatureAlgorithmId, + #[error("invalid proprietary certificate key algorithm ID")] + InvalidPropCertKeyAlgorithmId, + #[error("invalid RSA public key magic")] + InvalidRsaPublicKeyMagic, + #[error("invalid RSA public key length")] + InvalidRsaPublicKeyLength, + #[error("invalid RSA public key data length")] + InvalidRsaPublicKeyDataLength, + #[error("invalid RSA public key bit length")] + InvalidRsaPublicKeyBitLength, + #[error("invalid License Header security flags")] + InvalidSecurityFlags, + #[error("the server returned unexpected error: {0:?}")] + UnexpectedError(LicensingErrorMessage), + #[error("got unexpected license message")] + UnexpectedLicenseMessage, + #[error("the server has returned an unexpected error")] + UnexpectedServerError(LicensingErrorMessage), + #[error("the server has returned STATUS_VALID_CLIENT (not an error)")] + ValidClientStatus(LicensingErrorMessage), + #[error("invalid Key Exchange List field")] + InvalidKeyExchangeAlgorithm, + #[error("received invalid company name length (Product Information): {0}")] + InvalidCompanyNameLength(u32), + #[error("received invalid product ID length (Product Information): {0}")] + InvalidProductIdLength(u32), + #[error("received invalid scope count field: {0}")] + InvalidScopeCount(u32), + #[error("received invalid certificate length: {0}")] + InvalidCertificateLength(u32), + #[error("blob too small")] + BlobTooSmall, + #[error("PDU error: {0}")] + Pdu(PduError), +} + +impl From for ServerLicenseError { + fn from(e: PduError) -> Self { + Self::Pdu(e) + } +} + +impl From for ServerLicenseError { + fn from(e: LicensingErrorMessage) -> Self { + Self::UnexpectedError(e) + } +} + +#[derive(Debug, PartialEq)] +pub struct BlobHeader { + pub blob_type: BlobType, + pub length: usize, +} + +impl BlobHeader { + const NAME: &'static str = "BlobHeader"; + + const FIXED_PART_SIZE: usize = 2 /* blobType */ + 2 /* len */; + + pub fn new(blob_type: BlobType, length: usize) -> Self { + Self { blob_type, length } + } +} + +impl Encode for BlobHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.blob_type.0); + dst.write_u16(cast_length!("len", self.length)?); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for BlobHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let blob_type = BlobType(src.read_u16()); + let length = cast_length!("len", src.read_u16())?; + + Ok(Self { blob_type, length }) + } +} + +fn compute_mac_data(mac_salt_key: &[u8], data: &[u8]) -> Result, ServerLicenseError> { + let data_len_buffer = u32::try_from(data.len()) + .map_err(|_| ServerLicenseError::InvalidField("MAC data length"))? + .to_le_bytes(); + + let pad_one: [u8; 40] = [0x36; 40]; + + let mut hasher = sha1::Sha1::new(); + hasher.update( + [mac_salt_key, pad_one.as_ref(), data_len_buffer.as_ref(), data] + .concat() + .as_slice(), + ); + let sha_result = hasher.finalize(); + + let pad_two: [u8; 48] = [0x5c; 48]; + + let mut md5 = md5::Md5::new(); + md5.update( + [mac_salt_key, pad_two.as_ref(), sha_result.as_ref()] + .concat() + .as_slice(), + ); + + Ok(md5.finalize().to_vec()) +} + +#[derive(Debug, PartialEq)] +pub enum LicensePdu { + ClientNewLicenseRequest(ClientNewLicenseRequest), + ClientLicenseInfo(ClientLicenseInfo), + ClientPlatformChallengeResponse(ClientPlatformChallengeResponse), + ServerLicenseRequest(ServerLicenseRequest), + ServerPlatformChallenge(ServerPlatformChallenge), + ServerUpgradeLicense(ServerUpgradeLicense), + LicensingErrorMessage(LicensingErrorMessage), +} + +impl<'de> Decode<'de> for LicensePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let license_header = LicenseHeader::decode(src)?; + + match license_header.preamble_message_type { + PreambleType::LicenseRequest => Ok(ServerLicenseRequest::decode(license_header, src)?.into()), + PreambleType::PlatformChallenge => Ok(ServerPlatformChallenge::decode(license_header, src)?.into()), + PreambleType::NewLicense | PreambleType::UpgradeLicense => { + Ok(ServerUpgradeLicense::decode(license_header, src)?.into()) + } + PreambleType::LicenseInfo => Err(unsupported_value_err!( + "LicensePdu::LicenseInfo", + "LicenseInfo is not supported".to_owned() + )), + PreambleType::NewLicenseRequest => Ok(ClientNewLicenseRequest::decode(license_header, src)?.into()), + PreambleType::PlatformChallengeResponse => { + Ok(ClientPlatformChallengeResponse::decode(license_header, src)?.into()) + } + PreambleType::ErrorAlert => Ok(LicensingErrorMessage::decode(license_header, src)?.into()), + } + } +} + +impl Encode for LicensePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + Self::ClientNewLicenseRequest(ref pdu) => pdu.encode(dst), + Self::ClientLicenseInfo(ref pdu) => pdu.encode(dst), + Self::ClientPlatformChallengeResponse(ref pdu) => pdu.encode(dst), + Self::ServerLicenseRequest(ref pdu) => pdu.encode(dst), + Self::ServerPlatformChallenge(ref pdu) => pdu.encode(dst), + Self::ServerUpgradeLicense(ref pdu) => pdu.encode(dst), + Self::LicensingErrorMessage(ref pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + match self { + Self::ClientNewLicenseRequest(pdu) => pdu.name(), + Self::ClientLicenseInfo(pdu) => pdu.name(), + Self::ClientPlatformChallengeResponse(pdu) => pdu.name(), + Self::ServerLicenseRequest(pdu) => pdu.name(), + Self::ServerPlatformChallenge(pdu) => pdu.name(), + Self::ServerUpgradeLicense(pdu) => pdu.name(), + Self::LicensingErrorMessage(pdu) => pdu.name(), + } + } + + fn size(&self) -> usize { + match self { + Self::ClientNewLicenseRequest(pdu) => pdu.size(), + Self::ClientLicenseInfo(pdu) => pdu.size(), + Self::ClientPlatformChallengeResponse(pdu) => pdu.size(), + Self::ServerLicenseRequest(pdu) => pdu.size(), + Self::ServerPlatformChallenge(pdu) => pdu.size(), + Self::ServerUpgradeLicense(pdu) => pdu.size(), + Self::LicensingErrorMessage(pdu) => pdu.size(), + } + } +} + +impl From for LicensePdu { + fn from(pdu: ClientNewLicenseRequest) -> Self { + Self::ClientNewLicenseRequest(pdu) + } +} + +impl From for LicensePdu { + fn from(pdu: ClientLicenseInfo) -> Self { + Self::ClientLicenseInfo(pdu) + } +} + +impl From for LicensePdu { + fn from(pdu: ClientPlatformChallengeResponse) -> Self { + Self::ClientPlatformChallengeResponse(pdu) + } +} + +impl From for LicensePdu { + fn from(pdu: ServerLicenseRequest) -> Self { + Self::ServerLicenseRequest(pdu) + } +} + +impl From for LicensePdu { + fn from(pdu: ServerPlatformChallenge) -> Self { + Self::ServerPlatformChallenge(pdu) + } +} + +impl From for LicensePdu { + fn from(pdu: ServerUpgradeLicense) -> Self { + Self::ServerUpgradeLicense(pdu) + } +} + +impl From for LicensePdu { + fn from(pdu: LicensingErrorMessage) -> Self { + Self::LicensingErrorMessage(pdu) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/server_license_request/cert.rs b/crates/ironrdp-pdu/src/rdp/server_license/server_license_request/cert.rs new file mode 100644 index 00000000..f39a3098 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/server_license_request/cert.rs @@ -0,0 +1,247 @@ +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, write_padding, Decode, + DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; + +use super::{BlobHeader, BlobType, KEY_EXCHANGE_ALGORITHM_RSA}; + +pub const SIGNATURE_ALGORITHM_RSA: u32 = 1; +pub const PROP_CERT_NO_BLOBS_SIZE: usize = 8; +pub const PROP_CERT_BLOBS_HEADERS_SIZE: usize = 8; +pub const X509_CERT_LENGTH_FIELD_SIZE: usize = 4; +pub const X509_CERT_COUNT: usize = 4; +pub const RSA_KEY_PADDING_LENGTH: u32 = 8; +pub const RSA_SENTINEL: u32 = 0x3141_5352; +pub const RSA_KEY_SIZE_WITHOUT_MODULUS: usize = 20; + +const MIN_CERTIFICATE_AMOUNT: usize = 2; +const MAX_CERTIFICATE_AMOUNT: usize = 200; +const MAX_CERTIFICATE_LEN: usize = 4096; + +#[derive(Debug, PartialEq, Eq)] +pub enum CertificateType { + Proprietary(ProprietaryCertificate), + X509(X509CertificateChain), +} + +/// [2.2.1.4.2] X.509 Certificate Chain (X509 _CERTIFICATE_CHAIN) +/// +/// [2.2.1.4.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/bf2cc9cc-2b01-442e-a288-6ddfa3b80d59 +#[derive(Debug, PartialEq, Eq)] +pub struct X509CertificateChain { + pub certificate_array: Vec>, +} + +impl X509CertificateChain { + const NAME: &'static str = "X509CertificateChain"; +} + +impl Encode for X509CertificateChain { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(cast_length!("certArrayLen", self.certificate_array.len())?); + + for certificate in &self.certificate_array { + dst.write_u32(cast_length!("certLen", certificate.len())?); + dst.write_slice(certificate); + } + + let padding_len = 8 + 4 * self.certificate_array.len(); // MSDN: A byte array of the length 8 + 4*NumCertBlobs + write_padding!(dst, padding_len); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let certificates_length: usize = self + .certificate_array + .iter() + .map(|certificate| certificate.len() + X509_CERT_LENGTH_FIELD_SIZE) + .sum(); + let padding: usize = 8 + 4 * self.certificate_array.len(); + X509_CERT_COUNT + certificates_length + padding + } +} + +impl<'de> Decode<'de> for X509CertificateChain { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_size!(in: src, size: 4); + let certificate_count = cast_length!("certArrayLen", src.read_u32())?; + if !(MIN_CERTIFICATE_AMOUNT..MAX_CERTIFICATE_AMOUNT).contains(&certificate_count) { + return Err(invalid_field_err!("certArrayLen", "invalid x509 certificate amount")); + } + + let certificate_array: Vec<_> = core::iter::repeat_with(|| { + ensure_size!(in: src, size: 4); + let certificate_len = cast_length!("certLen", src.read_u32())?; + if certificate_len > MAX_CERTIFICATE_LEN { + return Err(invalid_field_err!("certLen", "invalid x509 certificate length")); + } + + ensure_size!(in: src, size: certificate_len); + let certificate = src.read_slice(certificate_len).into(); + + Ok(certificate) + }) + .take(certificate_count) + .collect::>()?; + + let padding = 8 + 4 * certificate_count; // MSDN: A byte array of the length 8 + 4*NumCertBlobs + ensure_size!(in: src, size: padding); + read_padding!(src, padding); + + Ok(Self { certificate_array }) + } +} + +/// [2.2.1.4.3.1.1] Server Proprietary Certificate (PROPRIETARYSERVERCERTIFICATE) +/// +/// [2.2.1.4.3.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/a37d449a-73ac-4f00-9b9d-56cefc954634 +#[derive(Debug, PartialEq, Eq)] +pub struct ProprietaryCertificate { + pub public_key: RsaPublicKey, + pub signature: Vec, +} + +impl ProprietaryCertificate { + const NAME: &'static str = "ProprietaryCertificate"; + + const FIXED_PART_SIZE: usize = PROP_CERT_BLOBS_HEADERS_SIZE + PROP_CERT_NO_BLOBS_SIZE; +} + +impl Encode for ProprietaryCertificate { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(SIGNATURE_ALGORITHM_RSA); + dst.write_u32(KEY_EXCHANGE_ALGORITHM_RSA); + + BlobHeader::new(BlobType::RSA_KEY, self.public_key.size()).encode(dst)?; + self.public_key.encode(dst)?; + + BlobHeader::new(BlobType::RSA_SIGNATURE, self.signature.len()).encode(dst)?; + dst.write_slice(&self.signature); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.public_key.size() + self.signature.len() + } +} + +impl<'de> Decode<'de> for ProprietaryCertificate { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_size!(in: src, size: PROP_CERT_NO_BLOBS_SIZE); + + let signature_algorithm_id = src.read_u32(); + if signature_algorithm_id != SIGNATURE_ALGORITHM_RSA { + return Err(invalid_field_err!("sigAlgId", "invalid signature algorithm ID")); + } + + let key_algorithm_id = src.read_u32(); + if key_algorithm_id != KEY_EXCHANGE_ALGORITHM_RSA { + return Err(invalid_field_err!("keyAlgId", "invalid key algorithm ID")); + } + + let key_blob_header = BlobHeader::decode(src)?; + if key_blob_header.blob_type != BlobType::RSA_KEY { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + let public_key = RsaPublicKey::decode(src)?; + + let sig_blob_header = BlobHeader::decode(src)?; + if sig_blob_header.blob_type != BlobType::RSA_SIGNATURE { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + ensure_size!(in: src, size: sig_blob_header.length); + let signature = src.read_slice(sig_blob_header.length).into(); + + Ok(Self { public_key, signature }) + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct RsaPublicKey { + pub public_exponent: u32, + pub modulus: Vec, +} + +impl RsaPublicKey { + const NAME: &'static str = "RsaPublicKey"; + + const FIXED_PART_SIZE: usize = RSA_KEY_SIZE_WITHOUT_MODULUS; +} + +impl Encode for RsaPublicKey { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let keylen = cast_length!("modulusLen", self.modulus.len())?; + let bitlen = (keylen - RSA_KEY_PADDING_LENGTH) * 8; + let datalen = bitlen / 8 - 1; + + dst.write_u32(RSA_SENTINEL); // magic + dst.write_u32(keylen); + dst.write_u32(bitlen); + dst.write_u32(datalen); + dst.write_u32(self.public_exponent); + dst.write_slice(&self.modulus); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.modulus.len() + } +} + +impl<'de> Decode<'de> for RsaPublicKey { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let magic = src.read_u32(); + if magic != RSA_SENTINEL { + return Err(invalid_field_err!("magic", "invalid RSA public key magic")); + } + + let keylen = cast_length!("keyLen", src.read_u32())?; + + let bitlen: usize = cast_length!("bitlen", src.read_u32())?; + if keylen != (bitlen / 8) + 8 { + return Err(invalid_field_err!("bitlen", "invalid RSA public key length")); + } + + if bitlen < 8 { + return Err(invalid_field_err!("bitlen", "invalid RSA public key length")); + } + + let datalen: usize = cast_length!("dataLen", src.read_u32())?; + if datalen != (bitlen / 8) - 1 { + return Err(invalid_field_err!("dataLen", "invalid RSA public key data length")); + } + + let public_exponent = src.read_u32(); + + ensure_size!(in: src, size: keylen); + let modulus = src.read_slice(keylen).into(); + + Ok(Self { + public_exponent, + modulus, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/server_license_request/mod.rs b/crates/ironrdp-pdu/src/rdp/server_license/server_license_request/mod.rs new file mode 100644 index 00000000..e4c5ee38 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/server_license_request/mod.rs @@ -0,0 +1,399 @@ +pub mod cert; + +#[cfg(test)] +mod tests; + +use cert::{CertificateType, ProprietaryCertificate, X509CertificateChain}; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; + +use super::{ + BlobHeader, BlobType, LicenseHeader, PreambleType, ServerLicenseError, BLOB_LENGTH_SIZE, BLOB_TYPE_SIZE, + KEY_EXCHANGE_ALGORITHM_RSA, RANDOM_NUMBER_SIZE, UTF16_NULL_TERMINATOR_SIZE, UTF8_NULL_TERMINATOR_SIZE, +}; +use crate::utils; + +const CERT_VERSION_FIELD_SIZE: usize = 4; +const KEY_EXCHANGE_FIELD_SIZE: usize = 4; +const SCOPE_ARRAY_SIZE_FIELD_SIZE: usize = 4; +const PRODUCT_INFO_STATIC_FIELDS_SIZE: usize = 12; +const CERT_CHAIN_VERSION_MASK: u32 = 0x7FFF_FFFF; +const CERT_CHAIN_ISSUED_MASK: u32 = 0x8000_0000; +const MAX_SCOPE_COUNT: u32 = 256; +const MAX_COMPANY_NAME_LEN: usize = 1024; +const MAX_PRODUCT_ID_LEN: usize = 1024; + +const RSA_EXCHANGE_ALGORITHM: u32 = 1; + +/// [2.2.2.1] Server License Request (SERVER_LICENSE_REQUEST) +/// +/// [2.2.2.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/e17772e9-9642-4bb6-a2bc-82875dd6da7c +#[derive(Debug, PartialEq, Eq)] +pub struct ServerLicenseRequest { + pub license_header: LicenseHeader, + pub server_random: Vec, + pub product_info: ProductInfo, + pub server_certificate: Option, + pub scope_list: Vec, +} + +impl ServerLicenseRequest { + const NAME: &'static str = "ServerLicenseRequest"; + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.license_header.encode(dst)?; + + dst.write_slice(&self.server_random); + self.product_info.encode(dst)?; + + BlobHeader::new(BlobType::KEY_EXCHANGE_ALGORITHM, KEY_EXCHANGE_FIELD_SIZE).encode(dst)?; + dst.write_u32(KEY_EXCHANGE_ALGORITHM_RSA); + + let cert_size = self.server_certificate.as_ref().map(|v| v.size()).unwrap_or(0); + BlobHeader::new(BlobType::CERTIFICATE, cert_size).encode(dst)?; + + if let Some(cert) = &self.server_certificate { + cert.encode(dst)?; + } + + dst.write_u32(cast_length!("listLen", self.scope_list.len())?); + + for scope in self.scope_list.iter() { + scope.encode(dst)?; + } + + Ok(()) + } + + pub fn decode(license_header: LicenseHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + if license_header.preamble_message_type != PreambleType::LicenseRequest { + return Err(invalid_field_err!("preambleMessageType", "unexpected preamble type")); + } + + ensure_size!(in: src, size: RANDOM_NUMBER_SIZE); + let server_random = src.read_slice(RANDOM_NUMBER_SIZE).into(); + + let product_info = ProductInfo::decode(src)?; + + let key_exchange_algorithm_blob = BlobHeader::decode(src)?; + if key_exchange_algorithm_blob.blob_type != BlobType::KEY_EXCHANGE_ALGORITHM { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + + ensure_size!(in: src, size: 4); + let key_exchange_algorithm = src.read_u32(); + if key_exchange_algorithm != RSA_EXCHANGE_ALGORITHM { + return Err(invalid_field_err!("keyAlgo", "invalid key exchange algorithm")); + } + + let cert_blob = BlobHeader::decode(src)?; + if cert_blob.blob_type != BlobType::CERTIFICATE { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + + // The terminal server can choose not to send the certificate by setting the wblobLen field in the Licensing Binary BLOB structure to 0 + let server_certificate = if cert_blob.length != 0 { + Some(ServerCertificate::decode(src)?) + } else { + None + }; + + ensure_size!(in: src, size: 4); + let scope_count = src.read_u32(); + if scope_count > MAX_SCOPE_COUNT { + return Err(invalid_field_err!("scopeCount", "invalid scope count")); + } + + let mut scope_list = Vec::with_capacity( + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer underflow)")] + usize::try_from(scope_count).expect("scope_count is guaranteed to fit into usize due to the prior check"), + ); + + for _ in 0..scope_count { + scope_list.push(Scope::decode(src)?); + } + + Ok(Self { + license_header, + server_random, + product_info, + server_certificate, + scope_list, + }) + } + + pub fn get_public_key(&self) -> Result>, ServerLicenseError> { + self.server_certificate.as_ref().map(|c| c.get_public_key()).transpose() + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + self.license_header.size() + + RANDOM_NUMBER_SIZE + + self.product_info.size() + + BLOB_LENGTH_SIZE * 2 // KeyExchangeBlob + CertificateBlob + + BLOB_TYPE_SIZE * 2 // KeyExchangeBlob + CertificateBlob + + KEY_EXCHANGE_FIELD_SIZE + + self.server_certificate.as_ref().map(|c| c.size()).unwrap_or(0) + + SCOPE_ARRAY_SIZE_FIELD_SIZE + + self.scope_list.iter().map(|s| s.size()).sum::() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Scope(pub String); + +impl Scope { + const NAME: &'static str = "Scope"; + + const FIXED_PART_SIZE: usize = BLOB_TYPE_SIZE + BLOB_LENGTH_SIZE; +} + +impl Encode for Scope { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let data_size = self.0.len() + UTF8_NULL_TERMINATOR_SIZE; + BlobHeader::new(BlobType::SCOPE, data_size).encode(dst)?; + dst.write_slice(self.0.as_bytes()); + dst.write_u8(0); // null terminator + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.0.len() + UTF8_NULL_TERMINATOR_SIZE + } +} + +impl<'de> Decode<'de> for Scope { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let blob_header = BlobHeader::decode(src)?; + if blob_header.blob_type != BlobType::SCOPE { + return Err(invalid_field_err!("blobType", "invalid blob type")); + } + if blob_header.length < UTF8_NULL_TERMINATOR_SIZE { + return Err(invalid_field_err!("blobLen", "blob too small")); + } + ensure_size!(in: src, size: blob_header.length); + let mut blob_data = src.read_slice(blob_header.length).to_vec(); + blob_data.resize(blob_data.len() - UTF8_NULL_TERMINATOR_SIZE, 0); + + if let Ok(data) = core::str::from_utf8(&blob_data) { + Ok(Self(String::from(data))) + } else { + Err(invalid_field_err!("scope", "scope is not utf8")) + } + } +} + +/// [2.2.1.4.3.1] Server Certificate (SERVER_CERTIFICATE) +/// +/// [2.2.1.4.3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/54e72cc6-3422-404c-a6b4-2486db125342 +#[derive(Debug, PartialEq, Eq)] +pub struct ServerCertificate { + pub issued_permanently: bool, + pub certificate: CertificateType, +} + +impl ServerCertificate { + const NAME: &'static str = "ServerCertificate"; + + const FIXED_PART_SIZE: usize = CERT_VERSION_FIELD_SIZE; + + pub fn get_public_key(&self) -> Result, ServerLicenseError> { + use x509_cert::der::Decode as _; + + match &self.certificate { + CertificateType::Proprietary(certificate) => { + let public_exponent = certificate.public_key.public_exponent.to_le_bytes(); + + let rsa_public_key = pkcs1::RsaPublicKey { + modulus: pkcs1::UintRef::new(&certificate.public_key.modulus)?, + public_exponent: pkcs1::UintRef::new(&public_exponent)?, + }; + + let public_key = pkcs1::der::Encode::to_der(&rsa_public_key)?; + + Ok(public_key) + } + CertificateType::X509(certificate) => { + let cert_der = certificate + .certificate_array + .last() + .ok_or_else(|| ServerLicenseError::InvalidX509CertificatesAmount)?; + + let cert = x509_cert::Certificate::from_der(cert_der).map_err(|source| { + ServerLicenseError::InvalidX509Certificate { + source, + cert_der: cert_der.clone(), + } + })?; + + let public_key = cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes() + .to_owned(); + + Ok(public_key) + } + } + } +} + +impl Encode for ServerCertificate { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let cert_version: u32 = match self.certificate { + CertificateType::Proprietary(_) => 1, + CertificateType::X509(_) => 2, + }; + let mask = if self.issued_permanently { + CERT_CHAIN_ISSUED_MASK + } else { + 0 + }; + + dst.write_u32(cert_version | mask); + + match &self.certificate { + CertificateType::Proprietary(cert) => cert.encode(dst)?, + CertificateType::X509(cert) => cert.encode(dst)?, + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let certificate_size = match &self.certificate { + CertificateType::Proprietary(cert) => cert.size(), + CertificateType::X509(cert) => cert.size(), + }; + + Self::FIXED_PART_SIZE + certificate_size + } +} + +impl<'de> Decode<'de> for ServerCertificate { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let cert_version = src.read_u32(); + + let issued_permanently = cert_version & CERT_CHAIN_ISSUED_MASK == CERT_CHAIN_ISSUED_MASK; + + let certificate = match cert_version & CERT_CHAIN_VERSION_MASK { + 1 => CertificateType::Proprietary(ProprietaryCertificate::decode(src)?), + 2 => CertificateType::X509(X509CertificateChain::decode(src)?), + _ => return Err(invalid_field_err!("certVersion", "invalid certificate version")), + }; + + Ok(Self { + issued_permanently, + certificate, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ProductInfo { + pub version: u32, + pub company_name: String, + pub product_id: String, +} + +impl ProductInfo { + const NAME: &'static str = "ProductInfo"; + + const FIXED_PART_SIZE: usize = PRODUCT_INFO_STATIC_FIELDS_SIZE; +} + +impl Encode for ProductInfo { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.version); + + let mut company_name = utils::to_utf16_bytes(&self.company_name); + company_name.resize(company_name.len() + 2, 0); + + dst.write_u32(cast_length!("companyLen", company_name.len())?); + dst.write_slice(&company_name); + + let mut product_id = utils::to_utf16_bytes(&self.product_id); + product_id.resize(product_id.len() + 2, 0); + + dst.write_u32(cast_length!("produceLen", product_id.len())?); + dst.write_slice(&product_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let company_name_utf_16 = utils::to_utf16_bytes(&self.company_name); + let product_id_utf_16 = utils::to_utf16_bytes(&self.product_id); + + Self::FIXED_PART_SIZE + + company_name_utf_16.len() + + UTF16_NULL_TERMINATOR_SIZE + + product_id_utf_16.len() + + UTF16_NULL_TERMINATOR_SIZE + } +} + +impl<'de> Decode<'de> for ProductInfo { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version = src.read_u32(); + + let company_name_len = cast_length!("companyLen", src.read_u32())?; + if !(2..=MAX_COMPANY_NAME_LEN).contains(&company_name_len) { + return Err(invalid_field_err!("companyLen", "invalid company name length")); + } + + ensure_size!(in: src, size: company_name_len); + let mut company_name = src.read_slice(company_name_len).to_vec(); + company_name.resize(company_name_len - 2, 0); + let company_name = utils::from_utf16_bytes(company_name.as_slice()); + + ensure_size!(in: src, size: 4); + let product_id_len = cast_length!("productIdLen", src.read_u32())?; + if !(2..=MAX_PRODUCT_ID_LEN).contains(&product_id_len) { + return Err(invalid_field_err!("productIdLen", "invalid produce ID length")); + } + + ensure_size!(in: src, size: product_id_len); + let mut product_id = src.read_slice(product_id_len).to_vec(); + product_id.resize(product_id_len - 2, 0); + let product_id = utils::from_utf16_bytes(product_id.as_slice()); + + Ok(Self { + version, + company_name, + product_id, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/server_license_request/tests.rs b/crates/ironrdp-pdu/src/rdp/server_license/server_license_request/tests.rs new file mode 100644 index 00000000..37bbd951 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/server_license_request/tests.rs @@ -0,0 +1,640 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::cert::{RsaPublicKey, PROP_CERT_BLOBS_HEADERS_SIZE, PROP_CERT_NO_BLOBS_SIZE, RSA_KEY_SIZE_WITHOUT_MODULUS}; +use super::*; +use crate::rdp::headers::{BasicSecurityHeader, BasicSecurityHeaderFlags}; +use crate::rdp::server_license::{LicensePdu, PreambleFlags, PreambleVersion}; + +const LICENSE_HEADER_BUFFER_WITH_CERT: [u8; 8] = [0x80, 0x00, 0x00, 0x00, 0x01, 0x03, 0x9C, 0x08]; +const LICENSE_HEADER_BUFFER_NO_CERT: [u8; 8] = [0x80, 0x00, 0x00, 0x00, 0x01, 0x03, 0x8A, 0x00]; + +const SERVER_RANDOM_BUFFER: [u8; 32] = [ + 0x84, 0xef, 0xae, 0x20, 0xb1, 0xd5, 0x9e, 0x36, 0x49, 0x1a, 0xe8, 0x2e, 0x0a, 0x99, 0x89, 0xac, 0x49, 0xa6, 0x47, + 0x4f, 0x33, 0x9b, 0x5a, 0xb9, 0x95, 0x03, 0xa6, 0xc6, 0xc2, 0x3c, 0x3f, 0x61, +]; + +const PRODUCT_INFO_BUFFER: [u8; 64] = [ + 0x00, 0x00, 0x06, 0x00, // version + 0x2c, 0x00, 0x00, 0x00, // company name len + 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x20, + 0x00, 0x43, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x70, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, 0x00, + 0x6f, 0x00, 0x6e, 0x00, 0x00, 0x00, // company name + 0x08, 0x00, 0x00, 0x00, // product id len + 0x41, 0x00, 0x30, 0x00, 0x32, 0x00, 0x00, 0x00, // product id +]; + +const KEY_EXCHANGE_LIST_BUFFER: [u8; 8] = [ + 0x0d, 0x00, // blob type + 0x04, 0x00, // blob len + 0x01, 0x00, 0x00, 0x00, // blob +]; + +const SERVER_CERTIFICATE_HEADER_BUFFER_WITH_BLOB: [u8; 8] = [ + 0x03, 0x00, // blob type + 0x12, 0x08, // blob len + 0x02, 0x00, 0x00, 0x80, // certificate version +]; + +const CERT_HEADER_BUFFER: [u8; 8] = [ + 0x02, 0x00, 0x00, 0x00, // num certificate + 0xf5, 0x02, 0x00, 0x00, // size of certificate +]; + +const CERT_1_BUFFER: [u8; 757] = [ + 0x30, 0x82, 0x02, 0xf1, 0x30, 0x82, 0x01, 0xdd, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x08, 0x01, 0x9e, 0x24, 0xa2, + 0xf2, 0xae, 0x90, 0x80, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1d, 0x05, 0x00, 0x30, 0x32, 0x31, 0x30, + 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1e, 0x0c, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x44, 0x00, 0x45, 0x00, 0x4e, + 0x00, 0x54, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x07, 0x1e, 0x12, 0x00, 0x57, 0x00, 0x4f, 0x00, 0x52, 0x00, 0x4b, + 0x00, 0x47, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x30, 0x1e, 0x17, 0x0d, 0x37, 0x30, 0x30, 0x35, 0x32, + 0x37, 0x30, 0x31, 0x31, 0x31, 0x30, 0x33, 0x5a, 0x17, 0x0d, 0x34, 0x39, 0x30, 0x35, 0x32, 0x37, 0x30, 0x31, 0x31, + 0x31, 0x30, 0x33, 0x5a, 0x30, 0x32, 0x31, 0x30, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1e, 0x0c, 0x00, 0x52, + 0x00, 0x4f, 0x00, 0x44, 0x00, 0x45, 0x00, 0x4e, 0x00, 0x54, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x07, 0x1e, 0x12, + 0x00, 0x57, 0x00, 0x4f, 0x00, 0x52, 0x00, 0x4b, 0x00, 0x47, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x30, + 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, + 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0x88, 0xad, 0x7c, 0x8f, 0x8b, 0x82, + 0x76, 0x5a, 0xbd, 0x8f, 0x6f, 0x62, 0x18, 0xe1, 0xd9, 0xaa, 0x41, 0xfd, 0xed, 0x68, 0x01, 0xc6, 0x34, 0x35, 0xb0, + 0x29, 0x04, 0xca, 0x4a, 0x4a, 0x1c, 0x7e, 0x80, 0x14, 0xf7, 0x8e, 0x77, 0xb8, 0x25, 0xff, 0x16, 0x47, 0x6f, 0xbd, + 0xe2, 0x34, 0x3d, 0x2e, 0x02, 0xb9, 0x53, 0xe4, 0x33, 0x75, 0xad, 0x73, 0x28, 0x80, 0xa0, 0x4d, 0xfc, 0x6c, 0xc0, + 0x22, 0x53, 0x1b, 0x2c, 0xf8, 0xf5, 0x01, 0x60, 0x19, 0x7e, 0x79, 0x19, 0x39, 0x8d, 0xb5, 0xce, 0x39, 0x58, 0xdd, + 0x55, 0x24, 0x3b, 0x55, 0x7b, 0x43, 0xc1, 0x7f, 0x14, 0x2f, 0xb0, 0x64, 0x3a, 0x54, 0x95, 0x2b, 0x88, 0x49, 0x0c, + 0x61, 0x2d, 0xac, 0xf8, 0x45, 0xf5, 0xda, 0x88, 0x18, 0x5f, 0xae, 0x42, 0xf8, 0x75, 0xc7, 0x26, 0x6d, 0xb5, 0xbb, + 0x39, 0x6f, 0xcc, 0x55, 0x1b, 0x32, 0x11, 0x38, 0x8d, 0xe4, 0xe9, 0x44, 0x84, 0x11, 0x36, 0xa2, 0x61, 0x76, 0xaa, + 0x4c, 0xb4, 0xe3, 0x55, 0x0f, 0xe4, 0x77, 0x8e, 0xde, 0xe3, 0xa9, 0xea, 0xb7, 0x41, 0x94, 0x00, 0x58, 0xaa, 0xc9, + 0x34, 0xa2, 0x98, 0xc6, 0x01, 0x1a, 0x76, 0x14, 0x01, 0xa8, 0xdc, 0x30, 0x7c, 0x77, 0x5a, 0x20, 0x71, 0x5a, 0xa2, + 0x3f, 0xaf, 0x13, 0x7e, 0xe8, 0xfd, 0x84, 0xa2, 0x5b, 0xcf, 0x25, 0xe9, 0xc7, 0x8f, 0xa8, 0xf2, 0x8b, 0x84, 0xc7, + 0x04, 0x5e, 0x53, 0x73, 0x4e, 0x0e, 0x89, 0xa3, 0x3c, 0xe7, 0x68, 0x5c, 0x24, 0xb7, 0x80, 0x53, 0x3c, 0x54, 0xc8, + 0xc1, 0x53, 0xaa, 0x71, 0x71, 0x3d, 0x36, 0x15, 0xd6, 0x6a, 0x9d, 0x7d, 0xde, 0xae, 0xf9, 0xe6, 0xaf, 0x57, 0xae, + 0xb9, 0x01, 0x96, 0x5d, 0xe0, 0x4d, 0xcd, 0xed, 0xc8, 0xd7, 0xf3, 0x01, 0x03, 0x38, 0x10, 0xbe, 0x7c, 0x42, 0x67, + 0x01, 0xa7, 0x23, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x13, 0x30, 0x11, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x13, + 0x04, 0x08, 0x30, 0x06, 0x01, 0x01, 0xff, 0x02, 0x01, 0x00, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1d, + 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x81, 0xdd, 0xd2, 0xd3, 0x33, 0xd4, 0xa3, 0xb6, 0x8e, 0x6e, 0x7d, 0x9f, + 0xfd, 0x73, 0x9f, 0x31, 0x0b, 0xdd, 0x42, 0x82, 0x3f, 0x7e, 0x21, 0xdf, 0x28, 0xcc, 0x59, 0xca, 0x6a, 0xc0, 0xa9, + 0x3d, 0x30, 0x7d, 0xe1, 0x91, 0xdb, 0x77, 0x6b, 0x8b, 0x10, 0xe6, 0xfd, 0xbc, 0x3c, 0xa3, 0x58, 0x48, 0xc2, 0x36, + 0xdd, 0xa0, 0x0b, 0xf5, 0x8e, 0x13, 0xda, 0x7b, 0x04, 0x08, 0x44, 0xb4, 0xf2, 0xa8, 0x0d, 0x1e, 0x0b, 0x1d, 0x1a, + 0x3f, 0xf9, 0x9b, 0x4b, 0x5a, 0x54, 0xc5, 0xb3, 0xb4, 0x03, 0x93, 0x75, 0xb3, 0x72, 0x5c, 0x3d, 0xcf, 0x63, 0x0f, + 0x15, 0xe1, 0x64, 0x58, 0xde, 0x52, 0x8d, 0x97, 0x79, 0x0e, 0xa4, 0x34, 0xd5, 0x66, 0x05, 0x58, 0xb8, 0x6e, 0x79, + 0xb2, 0x09, 0x86, 0xd5, 0xf0, 0xed, 0xc4, 0x6b, 0x4c, 0xab, 0x02, 0xb8, 0x16, 0x5f, 0x3b, 0xed, 0x88, 0x5f, 0xd1, + 0xde, 0x44, 0xe3, 0x73, 0x47, 0x21, 0xf7, 0x03, 0xce, 0xe1, 0x6d, 0x10, 0x0f, 0x95, 0xcf, 0x7c, 0xa2, 0x7a, 0xa6, + 0xbf, 0x20, 0xdb, 0xe1, 0x93, 0x04, 0xc8, 0x5e, 0x6a, 0xbe, 0xc8, 0x01, 0x5d, 0x27, 0xb2, 0x03, 0x0f, 0x66, 0x75, + 0xe7, 0xcb, 0xea, 0x8d, 0x4e, 0x98, 0x9d, 0x22, 0xed, 0x28, 0x40, 0xd2, 0x7d, 0xa4, 0x4b, 0xef, 0xcc, 0xbf, 0x01, + 0x2a, 0x6d, 0x3a, 0x3e, 0xbe, 0x47, 0x38, 0xf8, 0xea, 0xa4, 0xc6, 0x30, 0x1d, 0x5e, 0x25, 0xcf, 0xfb, 0xe8, 0x3d, + 0x42, 0xdd, 0x29, 0xe8, 0x99, 0x89, 0x9e, 0xbf, 0x39, 0xee, 0x77, 0x09, 0xd9, 0x3e, 0x8b, 0x52, 0x36, 0xb6, 0xbb, + 0x8b, 0xbd, 0x0d, 0xb2, 0x52, 0xaa, 0x2c, 0xcf, 0x38, 0x4e, 0x4d, 0xcf, 0x1d, 0x6d, 0x5d, 0x25, 0x17, 0xac, 0x2c, + 0xf6, 0xf0, 0x65, 0x5a, 0xc9, 0xfe, 0x31, 0x53, 0xb4, 0xf0, 0x0c, 0x94, 0x4e, 0x0d, 0x54, 0x8e, +]; + +const CERT_2_BUFFER: [u8; 1277] = [ + 0x30, 0x82, 0x04, 0xf9, 0x30, 0x82, 0x03, 0xe5, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x05, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1d, 0x05, 0x00, 0x30, 0x32, 0x31, 0x30, 0x30, 0x13, 0x06, + 0x03, 0x55, 0x04, 0x03, 0x1e, 0x0c, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x44, 0x00, 0x45, 0x00, 0x4e, 0x00, 0x54, 0x30, + 0x19, 0x06, 0x03, 0x55, 0x04, 0x07, 0x1e, 0x12, 0x00, 0x57, 0x00, 0x4f, 0x00, 0x52, 0x00, 0x4b, 0x00, 0x47, 0x00, + 0x52, 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x30, 0x1e, 0x17, 0x0d, 0x30, 0x37, 0x30, 0x35, 0x32, 0x36, 0x31, 0x32, + 0x34, 0x35, 0x35, 0x33, 0x5a, 0x17, 0x0d, 0x33, 0x38, 0x30, 0x31, 0x31, 0x39, 0x30, 0x33, 0x31, 0x34, 0x30, 0x37, + 0x5a, 0x30, 0x81, 0x92, 0x31, 0x81, 0x8f, 0x30, 0x23, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1e, 0x1c, 0x00, 0x6e, 0x00, + 0x63, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x72, 0x00, 0x70, 0x00, 0x63, 0x00, 0x3a, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x44, + 0x00, 0x45, 0x00, 0x4e, 0x00, 0x54, 0x30, 0x23, 0x06, 0x03, 0x55, 0x04, 0x07, 0x1e, 0x1c, 0x00, 0x6e, 0x00, 0x63, + 0x00, 0x61, 0x00, 0x6c, 0x00, 0x72, 0x00, 0x70, 0x00, 0x63, 0x00, 0x3a, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x44, 0x00, + 0x45, 0x00, 0x4e, 0x00, 0x54, 0x30, 0x43, 0x06, 0x03, 0x55, 0x04, 0x05, 0x1e, 0x3c, 0x00, 0x31, 0x00, 0x42, 0x00, + 0x63, 0x00, 0x4b, 0x00, 0x65, 0x00, 0x62, 0x00, 0x68, 0x00, 0x70, 0x00, 0x58, 0x00, 0x5a, 0x00, 0x74, 0x00, 0x4c, + 0x00, 0x71, 0x00, 0x4f, 0x00, 0x37, 0x00, 0x53, 0x00, 0x51, 0x00, 0x6e, 0x00, 0x42, 0x00, 0x70, 0x00, 0x52, 0x00, + 0x66, 0x00, 0x75, 0x00, 0x64, 0x00, 0x64, 0x00, 0x64, 0x00, 0x59, 0x00, 0x3d, 0x00, 0x0d, 0x00, 0x0a, 0x30, 0x82, + 0x01, 0x1e, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x0f, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, + 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xc8, 0x90, 0x6b, 0xf0, 0xc6, 0x58, 0x81, 0xa6, 0x89, 0x1c, 0x0e, + 0xf2, 0xf6, 0xd9, 0x82, 0x12, 0x71, 0xa5, 0x6e, 0x51, 0xdb, 0xe0, 0x32, 0x66, 0xaa, 0x91, 0x77, 0x0e, 0x88, 0xab, + 0x44, 0xb7, 0xd3, 0x97, 0xda, 0x78, 0x8f, 0x0e, 0x44, 0x26, 0x46, 0x7f, 0x16, 0xd4, 0xc6, 0x63, 0xeb, 0xca, 0x55, + 0xe5, 0x4e, 0x8b, 0x2d, 0xa6, 0x6d, 0x83, 0x95, 0xa7, 0xa8, 0x6a, 0xfa, 0xd0, 0xbe, 0x26, 0x80, 0xae, 0xab, 0x0a, + 0x64, 0x90, 0x32, 0x8c, 0xdf, 0x5c, 0xf8, 0xf9, 0xd0, 0x7e, 0xd1, 0x6b, 0x3a, 0x29, 0x7e, 0x7d, 0xbd, 0x02, 0xa3, + 0x86, 0x6c, 0xfd, 0xa5, 0x35, 0x71, 0xda, 0x21, 0xb4, 0xee, 0xa4, 0x97, 0xf3, 0xa8, 0xb2, 0x12, 0xdb, 0xa4, 0x27, + 0x57, 0x36, 0xc9, 0x08, 0x22, 0x5c, 0x54, 0xf7, 0x99, 0x7b, 0xa3, 0x2f, 0xb8, 0x5c, 0xd5, 0x16, 0xb8, 0x19, 0x27, + 0x6b, 0x71, 0x97, 0x14, 0x5b, 0xe8, 0x1f, 0x23, 0xe8, 0x5c, 0xb8, 0x1b, 0x73, 0x4b, 0x6e, 0x7a, 0x03, 0x13, 0xff, + 0x97, 0xe9, 0x62, 0xb9, 0x4a, 0xa0, 0x51, 0x23, 0xc3, 0x6c, 0x32, 0x3e, 0x02, 0xf2, 0x63, 0x97, 0x23, 0x1c, 0xc5, + 0x78, 0xd8, 0xfc, 0xb7, 0x07, 0x4b, 0xb0, 0x56, 0x0f, 0x74, 0xdf, 0xc5, 0x56, 0x28, 0xe4, 0x96, 0xfd, 0x20, 0x8e, + 0x65, 0x5a, 0xe6, 0x45, 0xed, 0xc1, 0x05, 0x3e, 0xab, 0x58, 0x55, 0x40, 0xaf, 0xe2, 0x47, 0xa0, 0x4c, 0x49, 0xa3, + 0x8d, 0x39, 0xe3, 0x66, 0x5f, 0x93, 0x33, 0x6d, 0xf8, 0x5f, 0xc5, 0x54, 0xe5, 0xfb, 0x57, 0x3a, 0xde, 0x45, 0x12, + 0xb5, 0xc7, 0x05, 0x4b, 0x88, 0x1f, 0xb4, 0x35, 0x0f, 0x7c, 0xc0, 0x75, 0x17, 0xc6, 0x67, 0xdd, 0x48, 0x80, 0xcb, + 0x0a, 0xbe, 0x9d, 0xf6, 0x93, 0x60, 0x65, 0x34, 0xeb, 0x97, 0xaf, 0x65, 0x6d, 0xdf, 0xbf, 0x6f, 0x5b, 0x02, 0x03, + 0x01, 0x00, 0x01, 0xa3, 0x82, 0x01, 0xbf, 0x30, 0x82, 0x01, 0xbb, 0x30, 0x14, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, + 0x01, 0x82, 0x37, 0x12, 0x04, 0x01, 0x01, 0xff, 0x04, 0x04, 0x01, 0x00, 0x05, 0x00, 0x30, 0x3c, 0x06, 0x09, 0x2b, + 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x02, 0x01, 0x01, 0xff, 0x04, 0x2c, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, + 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x20, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x72, + 0x00, 0x70, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x00, 0x00, + 0x30, 0x81, 0xcd, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x05, 0x01, 0x01, 0xff, 0x04, 0x81, + 0xbc, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x1c, 0x00, + 0x4a, 0x00, 0x66, 0x00, 0x4a, 0x00, 0xb0, 0x00, 0x01, 0x00, 0x33, 0x00, 0x64, 0x00, 0x32, 0x00, 0x36, 0x00, 0x37, + 0x00, 0x39, 0x00, 0x35, 0x00, 0x34, 0x00, 0x2d, 0x00, 0x65, 0x00, 0x65, 0x00, 0x62, 0x00, 0x37, 0x00, 0x2d, 0x00, + 0x31, 0x00, 0x31, 0x00, 0x64, 0x00, 0x31, 0x00, 0x2d, 0x00, 0x62, 0x00, 0x39, 0x00, 0x34, 0x00, 0x65, 0x00, 0x2d, + 0x00, 0x30, 0x00, 0x30, 0x00, 0x63, 0x00, 0x30, 0x00, 0x34, 0x00, 0x66, 0x00, 0x61, 0x00, 0x33, 0x00, 0x30, 0x00, + 0x38, 0x00, 0x30, 0x00, 0x64, 0x00, 0x00, 0x00, 0x33, 0x00, 0x64, 0x00, 0x32, 0x00, 0x36, 0x00, 0x37, 0x00, 0x39, + 0x00, 0x35, 0x00, 0x34, 0x00, 0x2d, 0x00, 0x65, 0x00, 0x65, 0x00, 0x62, 0x00, 0x37, 0x00, 0x2d, 0x00, 0x31, 0x00, + 0x31, 0x00, 0x64, 0x00, 0x31, 0x00, 0x2d, 0x00, 0x62, 0x00, 0x39, 0x00, 0x34, 0x00, 0x65, 0x00, 0x2d, 0x00, 0x30, + 0x00, 0x30, 0x00, 0x63, 0x00, 0x30, 0x00, 0x34, 0x00, 0x66, 0x00, 0x61, 0x00, 0x33, 0x00, 0x30, 0x00, 0x38, 0x00, + 0x30, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, + 0x6e, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x06, 0x01, 0x01, 0xff, 0x04, 0x5e, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x3e, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x44, 0x00, 0x45, 0x00, 0x4e, 0x00, 0x54, + 0x00, 0x00, 0x00, 0x37, 0x00, 0x38, 0x00, 0x34, 0x00, 0x34, 0x00, 0x30, 0x00, 0x2d, 0x00, 0x30, 0x00, 0x30, 0x00, + 0x36, 0x00, 0x2d, 0x00, 0x35, 0x00, 0x38, 0x00, 0x36, 0x00, 0x37, 0x00, 0x30, 0x00, 0x34, 0x00, 0x35, 0x00, 0x2d, + 0x00, 0x37, 0x00, 0x30, 0x00, 0x33, 0x00, 0x34, 0x00, 0x37, 0x00, 0x00, 0x00, 0x57, 0x00, 0x4f, 0x00, 0x52, 0x00, + 0x4b, 0x00, 0x47, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x25, 0x06, + 0x03, 0x55, 0x1d, 0x23, 0x01, 0x01, 0xff, 0x04, 0x1b, 0x30, 0x19, 0xa1, 0x10, 0xa4, 0x0e, 0x52, 0x00, 0x4f, 0x00, + 0x44, 0x00, 0x45, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x00, 0x00, 0x82, 0x05, 0x01, 0x00, 0x00, 0x00, 0x02, 0x30, 0x09, + 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1d, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x2e, 0xeb, 0xc7, 0x0d, 0xb8, + 0x1d, 0x47, 0x11, 0x9d, 0x09, 0x88, 0x9b, 0x51, 0xdc, 0x45, 0xdd, 0x56, 0x51, 0xe2, 0xd1, 0x23, 0x11, 0x39, 0x9b, + 0x2d, 0xda, 0xc7, 0xfe, 0x7a, 0xd7, 0x84, 0xe3, 0x3d, 0x54, 0x77, 0x97, 0x4d, 0x19, 0x92, 0x30, 0x64, 0xa0, 0x47, + 0xc6, 0x2f, 0x6d, 0x93, 0xd2, 0x64, 0x7c, 0x76, 0xc8, 0x26, 0x45, 0xad, 0x5a, 0x44, 0x54, 0xea, 0xf6, 0x4b, 0x28, + 0x77, 0x1f, 0x77, 0xea, 0xec, 0x74, 0x02, 0x38, 0x68, 0x9e, 0x79, 0x14, 0x72, 0x83, 0x34, 0x74, 0x62, 0xd2, 0xc1, + 0x0c, 0xa4, 0x0b, 0xf2, 0xa9, 0xb0, 0x38, 0xbb, 0x7c, 0xd0, 0xae, 0xbe, 0xbf, 0x74, 0x47, 0x16, 0xa0, 0xa2, 0xd3, + 0xfc, 0x1d, 0xb9, 0xba, 0x26, 0x10, 0x06, 0xef, 0xba, 0x1d, 0x43, 0x01, 0x4e, 0x4e, 0x6f, 0x56, 0xca, 0xe0, 0xee, + 0xd0, 0xf9, 0x4e, 0xa6, 0x62, 0x63, 0xff, 0xda, 0x0b, 0xc9, 0x15, 0x61, 0x6c, 0xed, 0x6b, 0x0b, 0xc4, 0x58, 0x53, + 0x86, 0x0f, 0x8c, 0x0c, 0x1a, 0x2e, 0xdf, 0xc1, 0xf2, 0x43, 0x48, 0xd4, 0xaf, 0x0a, 0x78, 0x36, 0xb2, 0x51, 0x32, + 0x28, 0x6c, 0xc2, 0x75, 0x79, 0x3f, 0x6e, 0x99, 0x66, 0x88, 0x3e, 0x34, 0xd3, 0x7f, 0x6d, 0x9d, 0x07, 0xe4, 0x6b, + 0xeb, 0x84, 0xe2, 0x0a, 0xbb, 0xca, 0x7d, 0x3a, 0x40, 0x71, 0xb0, 0xbe, 0x47, 0x9f, 0x12, 0x58, 0x31, 0x61, 0x2b, + 0x9b, 0x4a, 0x9a, 0x49, 0x8f, 0xe5, 0xb4, 0x0c, 0xf5, 0x04, 0x4d, 0x3c, 0xce, 0xbc, 0xd2, 0x79, 0x15, 0xd9, 0x28, + 0xf4, 0x23, 0x56, 0x77, 0x9f, 0x38, 0x64, 0x3e, 0x03, 0x88, 0x92, 0x04, 0x26, 0x76, 0xb9, 0xb5, 0xdf, 0x19, 0xd0, + 0x78, 0x4b, 0x7a, 0x60, 0x40, 0x23, 0x91, 0xf1, 0x15, 0x22, 0x2b, 0xb4, 0xe7, 0x02, 0x54, 0xa9, 0x16, 0x21, 0x5b, + 0x60, 0x96, 0xa9, 0x5c, +]; + +const CERT_2_LEN_BUFFER: [u8; 4] = [0xfd, 0x04, 0x00, 0x00]; + +const PADDING_BUFFER: [u8; 16] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding +]; + +const SCOPE_BUFFER_WITH_COUNT: [u8; 22] = [ + 0x01, 0x00, 0x00, 0x00, // scope count + 0x0e, 0x00, 0x0e, 0x00, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x2e, 0x63, 0x6f, 0x6d, + 0x00, // scope list +]; + +const MAGIC: [u8; 4] = [0x52, 0x53, 0x41, 0x31]; +const KEYLEN: [u8; 4] = [0x48, 0x00, 0x00, 0x00]; +const BITLEN: [u8; 4] = [0x00, 0x02, 0x00, 0x00]; +const DATALEN: [u8; 4] = [0x3f, 0x00, 0x00, 0x00]; +const PUB_EXP: [u8; 4] = [0x01, 0x00, 0x01, 0x00]; + +const MODULUS: [u8; 72] = [ + 0xcb, 0x81, 0xfe, 0xba, 0x6d, 0x61, 0xc3, 0x55, 0x05, 0xd5, 0x5f, 0x2e, 0x87, 0xf8, 0x71, 0x94, 0xd6, 0xf1, 0xa5, + 0xcb, 0xf1, 0x5f, 0x0c, 0x3d, 0xf8, 0x70, 0x02, 0x96, 0xc4, 0xfb, 0x9b, 0xc8, 0x3c, 0x2d, 0x55, 0xae, 0xe8, 0xff, + 0x32, 0x75, 0xea, 0x68, 0x79, 0xe5, 0xa2, 0x01, 0xfd, 0x31, 0xa0, 0xb1, 0x1f, 0x55, 0xa6, 0x1f, 0xc1, 0xf6, 0xd1, + 0x83, 0x88, 0x63, 0x26, 0x56, 0x12, 0xbc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +const CERT_HEADER_WITH_PARTIAL_RSA: [u8; 32] = [ + 0x01, 0x00, 0x00, 0x00, // sig + 0x01, 0x00, 0x00, 0x00, // dwkeyal + 0x06, 0x00, // blob type + 0x5c, 0x00, // len + 0x52, 0x53, 0x41, 0x31, 0x48, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, + 0x00, // rsa without modulus +]; + +const SIGNATURE: [u8; 72] = [ + 0xe9, 0xe1, 0xd6, 0x28, 0x46, 0x8b, 0x4e, 0xf5, 0x0a, 0xdf, 0xfd, 0xee, 0x21, 0x99, 0xac, 0xb4, 0xe1, 0x8f, 0x5f, + 0x81, 0x57, 0x82, 0xef, 0x9d, 0x96, 0x52, 0x63, 0x27, 0x18, 0x29, 0xdb, 0xb3, 0x4a, 0xfd, 0x9a, 0xda, 0x42, 0xad, + 0xb5, 0x69, 0x21, 0x89, 0x0e, 0x1d, 0xc0, 0x4c, 0x1a, 0xa8, 0xaa, 0x71, 0x3e, 0x0f, 0x54, 0xb9, 0x9a, 0xe4, 0x99, + 0x68, 0x3f, 0x6c, 0xd6, 0x76, 0x84, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +const SIGNATURE_BLOB_HEADER: [u8; 4] = [ + 0x08, 0x00, // sig blob type + 0x48, 0x00, // sig blob len +]; + +const SERVER_CERTIFICATE_HEADER_BUFFER: [u8; 4] = [ + 0x02, 0x00, 0x00, 0x80, // certificate version +]; + +const SCOPE_BUFFER: [u8; 18] = [ + 0x0e, 0x00, 0x0e, 0x00, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x2e, 0x63, 0x6f, 0x6d, + 0x00, // scope array +]; + +static PROPRIETARY_CERTIFICATE: LazyLock = LazyLock::new(|| ProprietaryCertificate { + public_key: RsaPublicKey { + public_exponent: 0x0001_0001, + modulus: Vec::from(MODULUS.as_ref()), + }, + signature: Vec::from(SIGNATURE.as_ref()), +}); +static PRODUCT_INFO: LazyLock = LazyLock::new(|| ProductInfo { + version: 0x60000, + company_name: "Microsoft Corporation".to_owned(), + product_id: "A02".to_owned(), +}); +static PUBLIC_KEY: LazyLock = LazyLock::new(|| RsaPublicKey { + public_exponent: 0x0001_0001, + modulus: Vec::from(MODULUS.as_ref()), +}); +static SERVER_LICENSE_REQUEST: LazyLock = LazyLock::new(|| { + let mut req = ServerLicenseRequest { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::LicenseRequest, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: 0, + }, + server_random: Vec::from(SERVER_RANDOM_BUFFER.as_ref()), + product_info: ProductInfo { + version: 0x60000, + company_name: "Microsoft Corporation".to_owned(), + product_id: "A02".to_owned(), + }, + server_certificate: Some(ServerCertificate { + issued_permanently: true, + certificate: CertificateType::X509(X509CertificateChain { + certificate_array: vec![Vec::from(CERT_1_BUFFER.as_ref()), Vec::from(CERT_2_BUFFER.as_ref())], + }), + }), + scope_list: vec![Scope(String::from("microsoft.com"))], + }; + req.license_header.preamble_message_size = u16::try_from(req.size()).expect("can't panic"); + req.into() +}); +static X509_CERTIFICATE: LazyLock = LazyLock::new(|| ServerCertificate { + issued_permanently: true, + certificate: CertificateType::X509(X509CertificateChain { + certificate_array: vec![Vec::from(CERT_1_BUFFER.as_ref()), Vec::from(CERT_2_BUFFER.as_ref())], + }), +}); +static SCOPE: LazyLock = LazyLock::new(|| Scope(String::from("microsoft.com"))); +static CERT_CHAIN: LazyLock = LazyLock::new(|| X509CertificateChain { + certificate_array: vec![Vec::from(CERT_1_BUFFER.as_ref()), Vec::from(CERT_2_BUFFER.as_ref())], +}); + +#[test] +fn from_buffer_correctly_parses_server_license_request() { + let request_buffer = [ + &LICENSE_HEADER_BUFFER_WITH_CERT[..], + &SERVER_RANDOM_BUFFER[..], + &PRODUCT_INFO_BUFFER[..], + &KEY_EXCHANGE_LIST_BUFFER[..], + &SERVER_CERTIFICATE_HEADER_BUFFER_WITH_BLOB[..], + &CERT_HEADER_BUFFER[..], + &CERT_1_BUFFER[..], + &CERT_2_LEN_BUFFER[..], + &CERT_2_BUFFER[..], + &PADDING_BUFFER[..], + &SCOPE_BUFFER_WITH_COUNT[..], + ] + .concat(); + + assert_eq!(*SERVER_LICENSE_REQUEST, decode(&request_buffer).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_server_license_request_no_certificate() { + let server_certificate_header_buffer = [ + 0x03, 0x00, // blob type + 0x00, 0x00, // blob len + ]; + + let request_buffer = [ + &LICENSE_HEADER_BUFFER_NO_CERT[..], + &SERVER_RANDOM_BUFFER[..], + &PRODUCT_INFO_BUFFER[..], + &KEY_EXCHANGE_LIST_BUFFER[..], + &server_certificate_header_buffer[..], + &SCOPE_BUFFER_WITH_COUNT[..], + ] + .concat(); + + let mut request = ServerLicenseRequest { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::LicenseRequest, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: 0, + }, + server_random: Vec::from(SERVER_RANDOM_BUFFER.as_ref()), + product_info: ProductInfo { + version: 0x60000, + company_name: "Microsoft Corporation".to_owned(), + product_id: "A02".to_owned(), + }, + server_certificate: None, + scope_list: vec![Scope(String::from("microsoft.com"))], + }; + request.license_header.preamble_message_size = u16::try_from(request.size()).expect("can't panic"); + let request: LicensePdu = request.into(); + + assert_eq!(request, decode(&request_buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_server_license_request() { + let request_buffer = [ + &LICENSE_HEADER_BUFFER_WITH_CERT[..], + &SERVER_RANDOM_BUFFER[..], + &PRODUCT_INFO_BUFFER[..], + &KEY_EXCHANGE_LIST_BUFFER[..], + &SERVER_CERTIFICATE_HEADER_BUFFER_WITH_BLOB[..], + &CERT_HEADER_BUFFER[..], + &CERT_1_BUFFER[..], + &CERT_2_LEN_BUFFER[..], + &CERT_2_BUFFER[..], + &PADDING_BUFFER[..], + &SCOPE_BUFFER_WITH_COUNT[..], + ] + .concat(); + + let mut request = ServerLicenseRequest { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::LicenseRequest, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: 0, + }, + server_random: Vec::from(SERVER_RANDOM_BUFFER.as_ref()), + product_info: ProductInfo { + version: 0x60000, + company_name: "Microsoft Corporation".to_owned(), + product_id: "A02".to_owned(), + }, + server_certificate: Some(ServerCertificate { + issued_permanently: true, + certificate: CertificateType::X509(X509CertificateChain { + certificate_array: vec![Vec::from(CERT_1_BUFFER.as_ref()), Vec::from(CERT_2_BUFFER.as_ref())], + }), + }), + scope_list: vec![Scope(String::from("microsoft.com"))], + }; + request.license_header.preamble_message_size = u16::try_from(request.size()).unwrap(); + let request: LicensePdu = request.into(); + + let serialized_request = encode_vec(&request).unwrap(); + + assert_eq!(serialized_request, request_buffer); +} + +#[test] +fn buffer_length_is_correct_for_server_license_request() { + let request_buffer = [ + &LICENSE_HEADER_BUFFER_WITH_CERT[..], + &SERVER_RANDOM_BUFFER[..], + &PRODUCT_INFO_BUFFER[..], + &KEY_EXCHANGE_LIST_BUFFER[..], + &SERVER_CERTIFICATE_HEADER_BUFFER_WITH_BLOB[..], + &CERT_HEADER_BUFFER[..], + &CERT_1_BUFFER[..], + &CERT_2_LEN_BUFFER[..], + &CERT_2_BUFFER[..], + &PADDING_BUFFER[..], + &SCOPE_BUFFER_WITH_COUNT[..], + ] + .concat(); + + assert_eq!(request_buffer.len(), SERVER_LICENSE_REQUEST.size()); +} + +#[test] +fn from_buffer_correctly_parses_rsa_public_key() { + let buffer = [ + &MAGIC[..], + &KEYLEN[..], + &BITLEN[..], + &DATALEN[..], + &PUB_EXP[..], + &MODULUS[..], + ] + .concat(); + + assert_eq!(*PUBLIC_KEY, decode(&buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_rsa_public_key() { + let buffer = [ + &MAGIC[..], + &KEYLEN[..], + &BITLEN[..], + &DATALEN[..], + &PUB_EXP[..], + &MODULUS[..], + ] + .concat(); + + let serialized_rsa_key = encode_vec(&*PUBLIC_KEY).unwrap(); + + assert_eq!(&serialized_rsa_key, &buffer); +} + +#[test] +fn buffer_length_is_correct_for_rsa_public_key() { + assert_eq!(PUBLIC_KEY.size(), RSA_KEY_SIZE_WITHOUT_MODULUS + MODULUS.len()); +} + +#[test] +fn from_buffer_correctly_parses_proprietary_certificate() { + let certificate_buffer = [ + &CERT_HEADER_WITH_PARTIAL_RSA[..], + &MODULUS[..], + &SIGNATURE_BLOB_HEADER[..], + &SIGNATURE[..], + ] + .concat(); + + assert_eq!(*PROPRIETARY_CERTIFICATE, decode(&certificate_buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_proprietary_certificate() { + let certificate_buffer = [ + &CERT_HEADER_WITH_PARTIAL_RSA[..], + &MODULUS[..], + &SIGNATURE_BLOB_HEADER[..], + &SIGNATURE[..], + ] + .concat(); + + let serialized_certificate = encode_vec(&*PROPRIETARY_CERTIFICATE).unwrap(); + + assert_eq!(serialized_certificate, certificate_buffer); +} + +#[test] +fn buffer_length_is_correct_for_proprietary_certificate() { + let certificate = ProprietaryCertificate { + public_key: PUBLIC_KEY.clone(), + signature: Vec::from(SIGNATURE.as_ref()), + }; + + assert_eq!( + certificate.size(), + PUBLIC_KEY.size() + SIGNATURE.len() + PROP_CERT_NO_BLOBS_SIZE + PROP_CERT_BLOBS_HEADERS_SIZE + ); +} + +#[test] +fn from_buffer_correctly_parses_product_information() { + assert_eq!(*PRODUCT_INFO, decode(&PRODUCT_INFO_BUFFER).unwrap()); +} + +#[test] +fn from_buffer_product_info_handles_invalid_strings_correctly() { + let product_info_buffer: [u8; 13] = [ + 0x00, 0x00, 0x06, 0x00, // version + 0x01, 0x00, 0x00, 0x00, // company name len + 0x00, // company name + 0x00, 0x00, 0x00, 0x00, // product id len + // product id + ]; + + assert!(decode::(product_info_buffer.as_ref()).is_err()); +} + +#[test] +fn to_buffer_correctly_serializes_product_information() { + let buffer = encode_vec(&*PRODUCT_INFO).unwrap(); + + assert_eq!(buffer, PRODUCT_INFO_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_product_information() { + assert_eq!(PRODUCT_INFO.size(), PRODUCT_INFO_BUFFER.len()); +} + +#[test] +fn get_public_key_correctly_gets_key_from_server_certificate() { + let public_key: [u8; 270] = [ + 0x30, 0x82, 0x1, 0xa, 0x2, 0x82, 0x1, 0x1, 0x0, 0xc8, 0x90, 0x6b, 0xf0, 0xc6, 0x58, 0x81, 0xa6, 0x89, 0x1c, + 0xe, 0xf2, 0xf6, 0xd9, 0x82, 0x12, 0x71, 0xa5, 0x6e, 0x51, 0xdb, 0xe0, 0x32, 0x66, 0xaa, 0x91, 0x77, 0xe, 0x88, + 0xab, 0x44, 0xb7, 0xd3, 0x97, 0xda, 0x78, 0x8f, 0xe, 0x44, 0x26, 0x46, 0x7f, 0x16, 0xd4, 0xc6, 0x63, 0xeb, + 0xca, 0x55, 0xe5, 0x4e, 0x8b, 0x2d, 0xa6, 0x6d, 0x83, 0x95, 0xa7, 0xa8, 0x6a, 0xfa, 0xd0, 0xbe, 0x26, 0x80, + 0xae, 0xab, 0xa, 0x64, 0x90, 0x32, 0x8c, 0xdf, 0x5c, 0xf8, 0xf9, 0xd0, 0x7e, 0xd1, 0x6b, 0x3a, 0x29, 0x7e, + 0x7d, 0xbd, 0x2, 0xa3, 0x86, 0x6c, 0xfd, 0xa5, 0x35, 0x71, 0xda, 0x21, 0xb4, 0xee, 0xa4, 0x97, 0xf3, 0xa8, + 0xb2, 0x12, 0xdb, 0xa4, 0x27, 0x57, 0x36, 0xc9, 0x8, 0x22, 0x5c, 0x54, 0xf7, 0x99, 0x7b, 0xa3, 0x2f, 0xb8, + 0x5c, 0xd5, 0x16, 0xb8, 0x19, 0x27, 0x6b, 0x71, 0x97, 0x14, 0x5b, 0xe8, 0x1f, 0x23, 0xe8, 0x5c, 0xb8, 0x1b, + 0x73, 0x4b, 0x6e, 0x7a, 0x3, 0x13, 0xff, 0x97, 0xe9, 0x62, 0xb9, 0x4a, 0xa0, 0x51, 0x23, 0xc3, 0x6c, 0x32, + 0x3e, 0x2, 0xf2, 0x63, 0x97, 0x23, 0x1c, 0xc5, 0x78, 0xd8, 0xfc, 0xb7, 0x7, 0x4b, 0xb0, 0x56, 0xf, 0x74, 0xdf, + 0xc5, 0x56, 0x28, 0xe4, 0x96, 0xfd, 0x20, 0x8e, 0x65, 0x5a, 0xe6, 0x45, 0xed, 0xc1, 0x5, 0x3e, 0xab, 0x58, + 0x55, 0x40, 0xaf, 0xe2, 0x47, 0xa0, 0x4c, 0x49, 0xa3, 0x8d, 0x39, 0xe3, 0x66, 0x5f, 0x93, 0x33, 0x6d, 0xf8, + 0x5f, 0xc5, 0x54, 0xe5, 0xfb, 0x57, 0x3a, 0xde, 0x45, 0x12, 0xb5, 0xc7, 0x5, 0x4b, 0x88, 0x1f, 0xb4, 0x35, 0xf, + 0x7c, 0xc0, 0x75, 0x17, 0xc6, 0x67, 0xdd, 0x48, 0x80, 0xcb, 0xa, 0xbe, 0x9d, 0xf6, 0x93, 0x60, 0x65, 0x34, + 0xeb, 0x97, 0xaf, 0x65, 0x6d, 0xdf, 0xbf, 0x6f, 0x5b, 0x2, 0x3, 0x1, 0x0, 0x1, + ]; + + assert_eq!( + public_key.as_ref(), + X509_CERTIFICATE.get_public_key().unwrap().as_slice() + ); +} + +#[test] +fn from_buffer_correctly_parses_server_certificate() { + let certificate_buffer = [ + &SERVER_CERTIFICATE_HEADER_BUFFER[..], + &CERT_HEADER_BUFFER[..], + &CERT_1_BUFFER[..], + &CERT_2_LEN_BUFFER[..], + &CERT_2_BUFFER[..], + &PADDING_BUFFER[..], + ] + .concat(); + + assert_eq!(*X509_CERTIFICATE, decode(&certificate_buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_server_certificate() { + let certificate_buffer = [ + &SERVER_CERTIFICATE_HEADER_BUFFER[..], + &CERT_HEADER_BUFFER[..], + &CERT_1_BUFFER[..], + &CERT_2_LEN_BUFFER[..], + &CERT_2_BUFFER[..], + &PADDING_BUFFER[..], + ] + .concat(); + + let serialized_certificate = encode_vec(&*X509_CERTIFICATE).unwrap(); + + assert_eq!(serialized_certificate, certificate_buffer); +} + +#[test] +fn buffer_length_is_correct_for_server_certificate() { + let certificate_buffer = [ + &SERVER_CERTIFICATE_HEADER_BUFFER[..], + &CERT_HEADER_BUFFER[..], + &CERT_1_BUFFER[..], + &CERT_2_LEN_BUFFER[..], + &CERT_2_BUFFER[..], + &PADDING_BUFFER[..], + ] + .concat(); + + assert_eq!(X509_CERTIFICATE.size(), certificate_buffer.len()); +} + +#[test] +fn from_buffer_correctly_parses_scope() { + assert_eq!(*SCOPE, decode(&SCOPE_BUFFER).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_scope() { + let serialized_scope = encode_vec(&*SCOPE).unwrap(); + + assert_eq!(serialized_scope, SCOPE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_scope() { + assert_eq!(SCOPE_BUFFER.len(), SCOPE.size()); +} + +#[test] +fn from_buffer_correctly_parses_x509_certificate_chain() { + let chain_buffer = [ + &CERT_HEADER_BUFFER[..], + &CERT_1_BUFFER[..], + &CERT_2_LEN_BUFFER[..], + &CERT_2_BUFFER[..], + &PADDING_BUFFER[..], + ] + .concat(); + + assert_eq!(*CERT_CHAIN, decode(&chain_buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_x509_certificate_chain() { + let chain_buffer = [ + &CERT_HEADER_BUFFER[..], + &CERT_1_BUFFER[..], + &CERT_2_LEN_BUFFER[..], + &CERT_2_BUFFER[..], + &PADDING_BUFFER[..], + ] + .concat(); + + let serialized_chain = encode_vec(&*CERT_CHAIN).unwrap(); + + assert_eq!(chain_buffer, serialized_chain); +} + +#[test] +fn buffer_length_is_correct_for_x509_certificate_chain() { + let chain_buffer = [ + &CERT_HEADER_BUFFER[..], + &CERT_1_BUFFER[..], + &CERT_2_LEN_BUFFER[..], + &CERT_2_BUFFER[..], + &PADDING_BUFFER[..], + ] + .concat(); + + assert_eq!(CERT_CHAIN.size(), chain_buffer.len()); +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/server_platform_challenge/mod.rs b/crates/ironrdp-pdu/src/rdp/server_license/server_platform_challenge/mod.rs new file mode 100644 index 00000000..1e8aabd0 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/server_platform_challenge/mod.rs @@ -0,0 +1,66 @@ +#[cfg(test)] +mod test; + +use ironrdp_core::{ + ensure_size, invalid_field_err, Decode as _, DecodeResult, Encode as _, EncodeResult, ReadCursor, WriteCursor, +}; + +use super::{BlobHeader, BlobType, LicenseHeader, PreambleType, BLOB_LENGTH_SIZE, BLOB_TYPE_SIZE, MAC_SIZE}; + +const CONNECT_FLAGS_FIELD_SIZE: usize = 4; + +/// [2.2.2.4] Server Platform Challenge (SERVER_PLATFORM_CHALLENGE) +/// +/// [2.2.2.4]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/41e129ad-0f35-43ad-a399-1b10e7d007a9 +#[derive(Debug, PartialEq, Eq)] +pub struct ServerPlatformChallenge { + pub license_header: LicenseHeader, + pub encrypted_platform_challenge: Vec, + pub mac_data: Vec, +} + +impl ServerPlatformChallenge { + const NAME: &'static str = "ServerPlatformChallenge"; + + const FIXED_PART_SIZE: usize = CONNECT_FLAGS_FIELD_SIZE + MAC_SIZE + BLOB_LENGTH_SIZE + BLOB_TYPE_SIZE; + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.license_header.encode(dst)?; + dst.write_u32(0); // connect_flags, ignored + BlobHeader::new(BlobType::ANY, self.encrypted_platform_challenge.len()).encode(dst)?; + dst.write_slice(&self.encrypted_platform_challenge); + dst.write_slice(&self.mac_data); + + Ok(()) + } + + pub fn decode(license_header: LicenseHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + if license_header.preamble_message_type != PreambleType::PlatformChallenge { + return Err(invalid_field_err!("preambleMessageType", "unexpected preamble type")); + } + + ensure_size!(in: src, size: 4); + let _connect_flags = src.read_u32(); + let blob_header = BlobHeader::decode(src)?; + ensure_size!(in: src, size: blob_header.length); + let encrypted_platform_challenge = src.read_slice(blob_header.length).into(); + ensure_size!(in: src, size: MAC_SIZE); + let mac_data = src.read_slice(MAC_SIZE).into(); + + Ok(Self { + license_header, + encrypted_platform_challenge, + mac_data, + }) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.license_header.size() + self.encrypted_platform_challenge.len() + } +} diff --git a/ironrdp/src/rdp/server_license/server_platform_challenge/test.rs b/crates/ironrdp-pdu/src/rdp/server_license/server_platform_challenge/test.rs similarity index 55% rename from ironrdp/src/rdp/server_license/server_platform_challenge/test.rs rename to crates/ironrdp-pdu/src/rdp/server_license/server_platform_challenge/test.rs index a8473128..3939b1d4 100644 --- a/ironrdp/src/rdp/server_license/server_platform_challenge/test.rs +++ b/crates/ironrdp-pdu/src/rdp/server_license/server_platform_challenge/test.rs @@ -1,21 +1,22 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + use super::*; use crate::rdp::server_license::{ - BasicSecurityHeader, BasicSecurityHeaderFlags, PreambleFlags, PreambleVersion, + BasicSecurityHeader, BasicSecurityHeaderFlags, LicensePdu, PreambleFlags, PreambleVersion, BASIC_SECURITY_HEADER_SIZE, }; -use lazy_static::lazy_static; - const PLATFORM_CHALLENGE_BUFFER: [u8; 42] = [ 0x80, 0x00, // flags 0x00, 0x00, // flagsHi 0x02, 0x03, 0x26, 0x00, // preamble - 0x00, 0x00, 0x00, 0x00, // connect flags - 0x00, 0x00, // ignored + 0x00, 0x00, 0x00, 0x00, // connect_flags (ignored) + 0x00, 0x00, // blob_type, ignored; 0x0a, 0x00, // blob len 0x46, 0x37, 0x85, 0x54, 0x8e, 0xc5, 0x91, 0x34, 0x97, 0x5d, // challenge - 0x38, 0x23, 0x62, 0x5d, 0x10, 0x8b, 0x93, 0xc3, 0xf1, 0xe4, 0x67, 0x1f, 0x4a, 0xb6, 0x00, - 0x0a, // mac data + 0x38, 0x23, 0x62, 0x5d, 0x10, 0x8b, 0x93, 0xc3, 0xf1, 0xe4, 0x67, 0x1f, 0x4a, 0xb6, 0x00, 0x0a, // mac data ]; const CHALLENGE_BUFFER: [u8; 10] = [ @@ -23,12 +24,11 @@ const CHALLENGE_BUFFER: [u8; 10] = [ ]; const MAC_DATA_BUFFER: [u8; MAC_SIZE] = [ - 0x38, 0x23, 0x62, 0x5d, 0x10, 0x8b, 0x93, 0xc3, 0xf1, 0xe4, 0x67, 0x1f, 0x4a, 0xb6, 0x00, - 0x0a, // mac data + 0x38, 0x23, 0x62, 0x5d, 0x10, 0x8b, 0x93, 0xc3, 0xf1, 0xe4, 0x67, 0x1f, 0x4a, 0xb6, 0x00, 0x0a, // mac data ]; -lazy_static! { - pub static ref PLATFORM_CHALLENGE: ServerPlatformChallenge = ServerPlatformChallenge { +static PLATFORM_CHALLENGE: LazyLock = LazyLock::new(|| { + ServerPlatformChallenge { license_header: LicenseHeader { security_header: BasicSecurityHeader { flags: BasicSecurityHeaderFlags::LICENSE_PKT, @@ -36,28 +36,32 @@ lazy_static! { preamble_message_type: PreambleType::PlatformChallenge, preamble_flags: PreambleFlags::empty(), preamble_version: PreambleVersion::V3, - preamble_message_size: (PLATFORM_CHALLENGE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE) - as u16, + preamble_message_size: u16::try_from(PLATFORM_CHALLENGE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE) + .expect("can't panic"), }, encrypted_platform_challenge: Vec::from(CHALLENGE_BUFFER.as_ref()), mac_data: Vec::from(MAC_DATA_BUFFER.as_ref()), - }; -} + } + .into() +}); #[test] fn from_buffer_correctly_parses_server_platform_challenge() { - assert_eq!( - *PLATFORM_CHALLENGE, - ServerPlatformChallenge::from_buffer(PLATFORM_CHALLENGE_BUFFER.as_ref()).unwrap() - ); + assert_eq!(*PLATFORM_CHALLENGE, decode(PLATFORM_CHALLENGE_BUFFER.as_ref()).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_server_platform_challenge_resiliently() { + let mut buffer = PLATFORM_CHALLENGE_BUFFER; + // Change blob type to junk value 0xbeef + buffer[12] = 0xbe; + buffer[13] = 0xef; + assert_eq!(*PLATFORM_CHALLENGE, decode(buffer.as_ref()).unwrap()); } #[test] fn to_buffer_correctly_serializes_server_platform_challenge() { - let mut serialized_platform_challenge = Vec::new(); - PLATFORM_CHALLENGE - .to_buffer(&mut serialized_platform_challenge) - .unwrap(); + let serialized_platform_challenge = encode_vec(&*PLATFORM_CHALLENGE).unwrap(); assert_eq!( PLATFORM_CHALLENGE_BUFFER.as_ref(), @@ -67,8 +71,5 @@ fn to_buffer_correctly_serializes_server_platform_challenge() { #[test] fn buffer_length_is_correct_for_server_platform_challenge() { - assert_eq!( - PLATFORM_CHALLENGE_BUFFER.len(), - PLATFORM_CHALLENGE.buffer_length() - ); + assert_eq!(PLATFORM_CHALLENGE_BUFFER.len(), PLATFORM_CHALLENGE.size()); } diff --git a/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/mod.rs b/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/mod.rs new file mode 100644 index 00000000..72671172 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/mod.rs @@ -0,0 +1,187 @@ +#[cfg(test)] +mod tests; + +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; + +use super::{ + BlobHeader, BlobType, LicenseEncryptionData, LicenseHeader, PreambleType, ServerLicenseError, BLOB_LENGTH_SIZE, + BLOB_TYPE_SIZE, MAC_SIZE, UTF16_NULL_TERMINATOR_SIZE, UTF8_NULL_TERMINATOR_SIZE, +}; +use crate::crypto::rc4::Rc4; +use crate::utils; +use crate::utils::CharacterSet; + +const LICENSE_INFO_STATIC_FIELDS_SIZE: usize = 20; + +/// [2.2.2.6] Server Upgrade License (SERVER_UPGRADE_LICENSE) +/// +/// [2.2.2.6]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpele/e8339fbd-1fe3-42c2-a599-27c04407166d +#[derive(Debug, PartialEq, Eq)] +pub struct ServerUpgradeLicense { + pub license_header: LicenseHeader, + pub encrypted_license_info: Vec, + pub mac_data: Vec, +} + +impl ServerUpgradeLicense { + const NAME: &'static str = "ServerUpgradeLicense"; + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.license_header.encode(dst)?; + BlobHeader::new(BlobType::ENCRYPTED_DATA, self.encrypted_license_info.len()).encode(dst)?; + dst.write_slice(&self.encrypted_license_info); + dst.write_slice(&self.mac_data); + + Ok(()) + } + + pub fn decode(license_header: LicenseHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + if license_header.preamble_message_type != PreambleType::UpgradeLicense + && license_header.preamble_message_type != PreambleType::NewLicense + { + return Err(invalid_field_err!( + "preambleType", + "got unexpected message preamble type" + )); + } + + let encrypted_license_info_blob = BlobHeader::decode(src)?; + if encrypted_license_info_blob.blob_type != BlobType::ENCRYPTED_DATA { + return Err(invalid_field_err!("blobType", "unexpected blob type")); + } + + ensure_size!(in: src, size: encrypted_license_info_blob.length + MAC_SIZE); + let encrypted_license_info = src.read_slice(encrypted_license_info_blob.length).into(); + let mac_data = src.read_slice(MAC_SIZE).into(); + + Ok(Self { + license_header, + encrypted_license_info, + mac_data, + }) + } + + pub fn verify_server_license(&self, encryption_data: &LicenseEncryptionData) -> Result<(), ServerLicenseError> { + let decrypted_license_info = self.decrypted_license_info(encryption_data); + let mac_data = + super::compute_mac_data(encryption_data.mac_salt_key.as_slice(), decrypted_license_info.as_ref())?; + + if mac_data != self.mac_data { + return Err(ServerLicenseError::InvalidMacData); + } + + Ok(()) + } + + pub fn new_license_info(&self, encryption_data: &LicenseEncryptionData) -> DecodeResult { + let data = self.decrypted_license_info(encryption_data); + LicenseInformation::decode(&mut ReadCursor::new(&data)) + } + + fn decrypted_license_info(&self, encryption_data: &LicenseEncryptionData) -> Vec { + let mut rc4 = Rc4::new(encryption_data.license_key.as_slice()); + rc4.process(self.encrypted_license_info.as_slice()) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + self.license_header.size() + BLOB_LENGTH_SIZE + BLOB_TYPE_SIZE + self.encrypted_license_info.len() + MAC_SIZE + } +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct LicenseInformation { + pub version: u32, + pub scope: String, + pub company_name: String, + pub product_id: String, + pub license_info: Vec, +} + +impl LicenseInformation { + const NAME: &'static str = "LicenseInformation"; + + const FIXED_PART_SIZE: usize = LICENSE_INFO_STATIC_FIELDS_SIZE; +} + +impl Encode for LicenseInformation { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.version); + + dst.write_u32(cast_length!("scopeLen", self.scope.len() + UTF8_NULL_TERMINATOR_SIZE)?); + utils::write_string_to_cursor(dst, &self.scope, CharacterSet::Ansi, true)?; + + dst.write_u32(cast_length!( + "companyLen", + self.company_name.len() * 2 + UTF16_NULL_TERMINATOR_SIZE + )?); + utils::write_string_to_cursor(dst, &self.company_name, CharacterSet::Unicode, true)?; + + dst.write_u32(cast_length!( + "produceIdLen", + self.product_id.len() * 2 + UTF16_NULL_TERMINATOR_SIZE + )?); + utils::write_string_to_cursor(dst, &self.product_id, CharacterSet::Unicode, true)?; + + dst.write_u32(cast_length!("licenseInfoLen", self.license_info.len())?); + dst.write_slice(self.license_info.as_slice()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + self.scope.len() + UTF8_NULL_TERMINATOR_SIZE + + self.company_name.len() * 2 // utf16 + + UTF16_NULL_TERMINATOR_SIZE + + self.product_id.len() * 2 // utf16 + + UTF16_NULL_TERMINATOR_SIZE + + self.license_info.len() + } +} + +impl<'de> Decode<'de> for LicenseInformation { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version = src.read_u32(); + + let scope_len: usize = cast_length!("scopeLen", src.read_u32())?; + ensure_size!(in: src, size: scope_len); + let scope = utils::decode_string(src.read_slice(scope_len), CharacterSet::Ansi, true)?; + + let company_name_len: usize = cast_length!("companyLen", src.read_u32())?; + ensure_size!(in: src, size: company_name_len); + let company_name = utils::decode_string(src.read_slice(company_name_len), CharacterSet::Unicode, true)?; + + let product_id_len: usize = cast_length!("productIdLen", src.read_u32())?; + ensure_size!(in: src, size: product_id_len); + let product_id = utils::decode_string(src.read_slice(product_id_len), CharacterSet::Unicode, true)?; + + let license_info_len = cast_length!("licenseInfoLen", src.read_u32())?; + ensure_size!(in: src, size: license_info_len); + let license_info = src.read_slice(license_info_len).into(); + + Ok(Self { + version, + scope, + company_name, + product_id, + license_info, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/tests.rs b/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/tests.rs new file mode 100644 index 00000000..a89d31c0 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/tests.rs @@ -0,0 +1,643 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; +use crate::rdp::server_license::{ + BasicSecurityHeader, BasicSecurityHeaderFlags, LicensePdu, PreambleFlags, PreambleVersion, + BASIC_SECURITY_HEADER_SIZE, PREAMBLE_SIZE, +}; + +const SERVER_UPGRADE_LICENSE_BUFFER: [u8; 2059] = [ + 0x80, 0x00, // flags + 0x00, 0x00, // flagsHi + 0x03, 0x03, 0x07, 0x08, // preamble + 0x09, 0x00, // blob type + 0xef, 0x07, // blob len + 0xdb, 0xa3, 0x13, 0x30, 0x79, 0xa3, 0xcd, 0x9e, 0x48, 0xf4, 0x8f, 0x06, 0x37, 0x1b, 0x45, 0xdd, 0x60, 0xa9, 0x2e, + 0x29, 0x26, 0xbf, 0xc1, 0x96, 0x5e, 0x07, 0x93, 0x9d, 0xf2, 0x2d, 0x3e, 0xa3, 0x3a, 0xff, 0xd5, 0x6d, 0xf5, 0x85, + 0x30, 0x28, 0xe1, 0x46, 0xfd, 0x56, 0xd1, 0x20, 0x41, 0x33, 0x94, 0x88, 0x0c, 0x27, 0x23, 0xa0, 0x61, 0x38, 0x60, + 0xdb, 0x86, 0xd6, 0xce, 0x2c, 0xcd, 0x40, 0x39, 0x55, 0x23, 0x39, 0x12, 0xb9, 0xfd, 0xc2, 0x8d, 0x58, 0x0a, 0x37, + 0x33, 0x42, 0x5c, 0x61, 0xd7, 0xc8, 0xa0, 0x11, 0x66, 0xe2, 0x45, 0xba, 0x41, 0x39, 0xea, 0x85, 0x2a, 0x6e, 0x7a, + 0xb3, 0xe7, 0x27, 0x75, 0xfc, 0x4d, 0xc0, 0xfb, 0x0d, 0xe8, 0x67, 0x90, 0xb3, 0x3a, 0x40, 0xf0, 0x15, 0x8a, 0x15, + 0x8e, 0x2c, 0x99, 0x0f, 0x1c, 0xbd, 0xd2, 0x08, 0x66, 0x51, 0x9e, 0x6a, 0xe6, 0x2c, 0xf7, 0x1f, 0xd0, 0xc0, 0x8e, + 0x89, 0x76, 0x64, 0x18, 0x58, 0xa1, 0x94, 0xbd, 0xce, 0xb1, 0x2d, 0x96, 0xab, 0x53, 0xcf, 0xf8, 0xbf, 0xd0, 0xc9, + 0xc0, 0x2e, 0xe6, 0xa4, 0x0b, 0x50, 0x31, 0x4a, 0x4e, 0xd8, 0x47, 0x4b, 0xaf, 0xb8, 0x21, 0x78, 0xbf, 0x09, 0xac, + 0x7f, 0x2d, 0x2d, 0x88, 0xf6, 0xd8, 0xc7, 0x45, 0x33, 0x9f, 0xac, 0x69, 0xf5, 0x88, 0x9d, 0x5c, 0x6e, 0xc9, 0xd0, + 0xca, 0x8c, 0xbc, 0xa9, 0xd6, 0x07, 0x36, 0xed, 0x40, 0x95, 0x8a, 0xc1, 0x3f, 0x04, 0x41, 0xb3, 0xc9, 0xb3, 0x18, + 0x9d, 0x33, 0x1b, 0x04, 0x55, 0xcd, 0x41, 0xdf, 0x19, 0xe1, 0xcd, 0xa0, 0xa4, 0x35, 0x6e, 0xb7, 0x0a, 0xf3, 0xec, + 0x48, 0x10, 0x4f, 0x28, 0xc6, 0x35, 0xf3, 0x9b, 0xa2, 0xd5, 0xf7, 0x58, 0x03, 0x4d, 0x9a, 0x16, 0x34, 0xfb, 0x96, + 0x0c, 0xd5, 0x3a, 0xae, 0x52, 0x1b, 0x2f, 0x1f, 0x1f, 0x31, 0xb2, 0xd9, 0x14, 0x3b, 0x73, 0x0f, 0xe3, 0x04, 0xe0, + 0xa5, 0x52, 0x89, 0x68, 0xba, 0x0f, 0x99, 0x9d, 0x24, 0xa6, 0xf3, 0xe8, 0x9f, 0xcc, 0xd2, 0x44, 0x9f, 0x08, 0x8b, + 0x0a, 0x24, 0x89, 0xf7, 0xc9, 0x07, 0x0d, 0x25, 0x07, 0xed, 0x3e, 0x75, 0x21, 0x19, 0x65, 0xdc, 0x98, 0x41, 0x9d, + 0x05, 0x12, 0x18, 0x88, 0x86, 0x16, 0x43, 0x49, 0x29, 0xf2, 0xe8, 0x26, 0x16, 0x1e, 0xce, 0xcd, 0x32, 0xe7, 0x36, + 0x74, 0x51, 0x27, 0xfd, 0xa2, 0xa9, 0x62, 0x57, 0x60, 0x28, 0xe4, 0x64, 0x02, 0x06, 0x6b, 0xff, 0x01, 0xab, 0xc5, + 0x1c, 0x25, 0x98, 0x07, 0xe1, 0x40, 0xad, 0x19, 0xb7, 0x68, 0x66, 0x12, 0x4e, 0x80, 0xbc, 0x83, 0xd2, 0xde, 0xcb, + 0x7e, 0xc2, 0x32, 0xc7, 0xb8, 0x4d, 0xd6, 0x7d, 0xdd, 0x63, 0xa9, 0x95, 0x45, 0xc1, 0x90, 0xc7, 0x99, 0x3c, 0x0a, + 0x24, 0x62, 0xfc, 0x24, 0x15, 0xdb, 0xd3, 0xd2, 0x9b, 0x5d, 0x78, 0x04, 0x78, 0xd5, 0x40, 0x1d, 0xe3, 0x4e, 0xe8, + 0x30, 0x9f, 0x56, 0x91, 0x71, 0x00, 0x86, 0x2c, 0x6a, 0xb2, 0x78, 0xec, 0x70, 0xd9, 0x71, 0xe6, 0xaa, 0xb1, 0xad, + 0x18, 0xf9, 0xa6, 0x84, 0xb7, 0x4b, 0x5f, 0x32, 0xb8, 0xe3, 0xc7, 0x84, 0xef, 0x37, 0xfe, 0xae, 0x99, 0xb5, 0xf2, + 0x34, 0x84, 0x82, 0x4a, 0xb3, 0xd0, 0x7c, 0x5e, 0x25, 0x71, 0x89, 0x8b, 0x7d, 0x6f, 0x5f, 0x96, 0x7a, 0x1d, 0x84, + 0x96, 0x56, 0x34, 0x30, 0xce, 0x09, 0xd5, 0x00, 0xa8, 0xac, 0x15, 0x72, 0x21, 0xc4, 0x71, 0x57, 0xe5, 0x2a, 0x3d, + 0xdf, 0x82, 0xb8, 0xb8, 0x63, 0xdc, 0x3f, 0x2e, 0x99, 0x6c, 0xc3, 0xe3, 0xfd, 0x92, 0xe0, 0x26, 0xe1, 0x27, 0xb8, + 0x04, 0x71, 0xb0, 0xa8, 0xd1, 0xdf, 0x7e, 0x24, 0x23, 0xb9, 0x82, 0x01, 0x77, 0xdc, 0x8f, 0x77, 0x54, 0xe6, 0x93, + 0xc8, 0x6c, 0x66, 0x87, 0xb7, 0xaa, 0x9d, 0x66, 0xd4, 0xc6, 0x2f, 0x5e, 0x9e, 0xe1, 0xcf, 0xdb, 0xb2, 0x74, 0x0e, + 0xea, 0xa5, 0xe0, 0xf7, 0x00, 0xf1, 0x76, 0xf7, 0x45, 0x2c, 0xf8, 0xa9, 0x3b, 0xd9, 0x81, 0x59, 0x52, 0x0f, 0xfe, + 0xd9, 0x28, 0x02, 0x59, 0x82, 0x39, 0x51, 0x6e, 0xb9, 0xac, 0xf9, 0x6a, 0x48, 0x73, 0x6f, 0x2c, 0x4d, 0x7b, 0xc0, + 0xbf, 0xbe, 0x69, 0xae, 0x0e, 0xdc, 0x8b, 0xe6, 0xd8, 0x9f, 0x66, 0x30, 0x1e, 0x45, 0x1d, 0x85, 0x23, 0xeb, 0xa8, + 0x02, 0xb5, 0xba, 0xc2, 0xfd, 0xa1, 0xff, 0xc5, 0x55, 0x2b, 0xa0, 0xf7, 0x5b, 0x24, 0xee, 0x81, 0xd8, 0xe1, 0xb8, + 0x02, 0x06, 0x85, 0x6e, 0x41, 0x5a, 0xb8, 0x07, 0xff, 0x65, 0xdb, 0xb4, 0x59, 0x89, 0x71, 0x95, 0xd5, 0x0c, 0x2a, + 0x67, 0x4d, 0x57, 0xfd, 0x4a, 0xe8, 0x07, 0x02, 0x42, 0x20, 0xd9, 0xf1, 0xc6, 0xd5, 0x4c, 0x53, 0xb0, 0x32, 0x68, + 0xc0, 0xdc, 0xd7, 0x5d, 0x8f, 0xec, 0x24, 0x29, 0x00, 0x4f, 0x46, 0x8d, 0xd2, 0x99, 0xb2, 0xf4, 0x06, 0x99, 0x9a, + 0xa6, 0x31, 0xf1, 0x49, 0x16, 0xfe, 0x94, 0xbb, 0x8e, 0x15, 0x55, 0x06, 0x93, 0x16, 0xa3, 0x2d, 0x10, 0xb7, 0xb1, + 0xcf, 0x61, 0x78, 0xaf, 0x93, 0x66, 0x5a, 0x75, 0x5e, 0x97, 0xc0, 0x97, 0x4c, 0xba, 0xa9, 0x50, 0xac, 0x1b, 0xd6, + 0x92, 0x2a, 0xac, 0x0a, 0x21, 0x12, 0x9e, 0x4a, 0xf0, 0x40, 0x39, 0x4b, 0xe5, 0x78, 0x88, 0x86, 0x17, 0xb9, 0xeb, + 0xa0, 0x33, 0x8a, 0x9a, 0xfc, 0x7c, 0x91, 0x16, 0xd7, 0x52, 0xec, 0x05, 0x7e, 0x4e, 0x90, 0x78, 0x5e, 0x45, 0x4a, + 0xdd, 0xf6, 0xf4, 0x2e, 0x68, 0xf7, 0x8e, 0xfc, 0x60, 0x95, 0xaa, 0x6a, 0x07, 0x9c, 0xea, 0xce, 0xc1, 0xd9, 0x55, + 0x3a, 0x78, 0x54, 0x9a, 0x2a, 0x5f, 0x47, 0x87, 0x18, 0x4a, 0x8c, 0x6c, 0x34, 0xf5, 0xb8, 0xe2, 0x84, 0x36, 0xef, + 0x0d, 0x2e, 0x9d, 0x42, 0xd9, 0xff, 0x56, 0xe2, 0x87, 0x0b, 0x2f, 0x4d, 0x0e, 0xc0, 0x60, 0x35, 0x06, 0x9f, 0x61, + 0x9e, 0x4e, 0x7b, 0x49, 0x41, 0xb4, 0xfa, 0x04, 0x10, 0xbd, 0xf6, 0xad, 0x02, 0xd9, 0x7c, 0xba, 0x06, 0x68, 0xbb, + 0xa7, 0xa6, 0x8a, 0xab, 0xab, 0xb1, 0x2d, 0x69, 0x2a, 0xf1, 0xc6, 0x7b, 0x1b, 0x71, 0xb9, 0xd0, 0x91, 0x82, 0x6f, + 0xa8, 0x3c, 0xe1, 0xa3, 0x23, 0x3d, 0x4e, 0x48, 0x74, 0xe5, 0xc9, 0xc5, 0x95, 0x31, 0xad, 0xe7, 0xa9, 0xdb, 0x35, + 0xcd, 0x02, 0x08, 0x2c, 0x29, 0x5f, 0xf9, 0x17, 0x86, 0x69, 0x8f, 0x13, 0xd1, 0xca, 0x83, 0xfc, 0xac, 0x55, 0xcf, + 0x5a, 0xe6, 0x45, 0xaf, 0xe5, 0xbb, 0xe7, 0xb5, 0x53, 0x4e, 0xf0, 0x63, 0xfc, 0x9a, 0x49, 0xf6, 0x45, 0x93, 0xc6, + 0xbf, 0xd5, 0xb3, 0x25, 0xe2, 0x93, 0xb0, 0xa6, 0xa7, 0x14, 0x80, 0x6d, 0xb2, 0x03, 0x15, 0x6a, 0xad, 0xe8, 0x25, + 0xf1, 0x80, 0xd4, 0xba, 0x9a, 0x88, 0xbc, 0x56, 0x14, 0x7a, 0x4d, 0xad, 0xc3, 0x24, 0x3f, 0x4d, 0x35, 0x8b, 0xf6, + 0x59, 0x5b, 0xfd, 0xc9, 0x32, 0x1d, 0xf5, 0xa5, 0x53, 0xb5, 0xfb, 0xba, 0x83, 0x29, 0x0b, 0x9c, 0x62, 0x9b, 0x56, + 0x4b, 0x44, 0xbc, 0xcc, 0x19, 0x59, 0x7c, 0x0b, 0x74, 0xd9, 0x04, 0x28, 0xb3, 0x67, 0xab, 0x82, 0x36, 0x39, 0x55, + 0x5f, 0x7c, 0xed, 0x84, 0xc1, 0x16, 0xd3, 0x9e, 0x9c, 0x90, 0x9d, 0x55, 0xbc, 0x3e, 0xb9, 0x63, 0x12, 0xf2, 0x26, + 0x6d, 0xd7, 0xcc, 0x4f, 0x01, 0xa2, 0x0b, 0xd9, 0x66, 0x60, 0xad, 0xed, 0x2e, 0xbd, 0xbe, 0x28, 0x5f, 0x4a, 0x33, + 0xc8, 0xe8, 0xd4, 0xa6, 0x23, 0x8a, 0xfd, 0x66, 0xf5, 0x28, 0x90, 0x81, 0x27, 0xa9, 0x44, 0x93, 0x68, 0x57, 0x44, + 0x5e, 0xba, 0x90, 0x12, 0x03, 0x15, 0x2f, 0x69, 0x80, 0x55, 0xe8, 0x32, 0x63, 0x88, 0x30, 0x85, 0x50, 0x9b, 0xb6, + 0xbc, 0xbb, 0xc6, 0xfe, 0xa2, 0xe7, 0x32, 0x9d, 0x3d, 0x7d, 0xe2, 0x31, 0x93, 0xa6, 0x4e, 0xa0, 0xdc, 0x11, 0x44, + 0xd2, 0x93, 0x32, 0x94, 0x1e, 0xc6, 0x4c, 0x28, 0xde, 0xa2, 0xa6, 0x0d, 0x14, 0x02, 0x74, 0x8a, 0x84, 0x2a, 0x03, + 0x67, 0x35, 0x1d, 0x66, 0x3e, 0xe9, 0x68, 0x4a, 0xb2, 0x92, 0x1a, 0x69, 0x48, 0xbd, 0x23, 0xfd, 0x70, 0x5a, 0xfd, + 0xfe, 0x74, 0x39, 0xc5, 0xfa, 0x11, 0xac, 0x04, 0xc9, 0x94, 0xfc, 0x12, 0x2e, 0x06, 0x04, 0x61, 0x8e, 0x32, 0xf4, + 0xf8, 0x3b, 0xd1, 0xd8, 0x09, 0xb3, 0xe4, 0xac, 0x0f, 0x3e, 0x92, 0xf7, 0x75, 0x0b, 0x32, 0x9b, 0xd4, 0x8a, 0x13, + 0x99, 0x6a, 0x26, 0x77, 0x9f, 0x34, 0x08, 0xa8, 0xeb, 0xb3, 0x3e, 0x2a, 0x5b, 0x4a, 0x44, 0xf9, 0x21, 0x89, 0x2a, + 0x09, 0x9c, 0xc7, 0x0d, 0x2a, 0xd8, 0xd6, 0x27, 0x30, 0x25, 0x39, 0x84, 0x14, 0x11, 0x47, 0xff, 0x60, 0xe4, 0x7c, + 0xc3, 0x41, 0xfd, 0xd5, 0x34, 0xdd, 0xb1, 0x1c, 0xf2, 0xeb, 0xb1, 0x67, 0x04, 0xfa, 0xfd, 0x65, 0x5b, 0x20, 0x6e, + 0x28, 0x75, 0xa9, 0x74, 0x8e, 0xc4, 0x2f, 0xf7, 0xb2, 0xf5, 0x9f, 0x13, 0x44, 0xff, 0xb0, 0xf0, 0x68, 0xb0, 0x69, + 0x15, 0xa6, 0x16, 0xa8, 0xac, 0xc3, 0x06, 0x14, 0x8e, 0x51, 0x99, 0xa9, 0x4c, 0x19, 0xd1, 0x25, 0x34, 0xb5, 0x79, + 0xc2, 0xa7, 0xbf, 0xd8, 0x3d, 0x2d, 0x4c, 0x33, 0xac, 0x1b, 0x6c, 0xaf, 0x10, 0x42, 0x41, 0x14, 0x02, 0xe6, 0x87, + 0x2b, 0xe9, 0xec, 0xc6, 0xb1, 0xeb, 0x97, 0xd4, 0x35, 0x49, 0x97, 0xfc, 0xe2, 0x73, 0xf9, 0x98, 0x46, 0x7c, 0xf6, + 0x17, 0x2d, 0xb5, 0x43, 0x07, 0x8b, 0x19, 0x95, 0x9b, 0x65, 0xd3, 0x05, 0x7e, 0xb0, 0x68, 0x0d, 0x6e, 0x4b, 0x60, + 0xad, 0x5c, 0x47, 0x6e, 0x37, 0xfd, 0x3f, 0x60, 0x43, 0xda, 0xb2, 0x34, 0x00, 0xd6, 0x9c, 0x6d, 0x46, 0x7f, 0x41, + 0xe2, 0xc1, 0x1a, 0xd0, 0x53, 0x72, 0x81, 0x0b, 0x3f, 0x77, 0xe1, 0xbc, 0xcc, 0x09, 0x0f, 0xa1, 0x1d, 0x73, 0x8c, + 0xac, 0xa4, 0x48, 0x90, 0x80, 0xa8, 0x50, 0x63, 0x6a, 0xb7, 0x76, 0x91, 0x91, 0x2f, 0x1a, 0x5e, 0x83, 0x80, 0xe6, + 0xae, 0x66, 0x77, 0x44, 0xe0, 0x0f, 0x14, 0x70, 0xc9, 0xd3, 0x91, 0xe8, 0xd2, 0xc4, 0x89, 0xa8, 0x45, 0xc0, 0x3d, + 0xbd, 0x09, 0x58, 0xe0, 0xcd, 0xe6, 0x5c, 0x9e, 0x02, 0x94, 0xd2, 0xbe, 0xdf, 0x94, 0x35, 0xf7, 0x67, 0x96, 0x75, + 0x88, 0x08, 0x59, 0xd9, 0x19, 0x21, 0xda, 0xd0, 0xa2, 0x74, 0x2d, 0x22, 0x87, 0x37, 0x27, 0x6e, 0x58, 0xdc, 0x8e, + 0x9f, 0x50, 0xd5, 0x62, 0xf3, 0x4a, 0xa4, 0xb2, 0xfb, 0xf9, 0x3e, 0xd5, 0xda, 0x57, 0x56, 0x5c, 0xcb, 0x0e, 0xd1, + 0x62, 0x4f, 0xea, 0x42, 0x4a, 0x62, 0xb2, 0x4e, 0x1c, 0xd1, 0xcc, 0x24, 0x1b, 0xdc, 0xac, 0xd4, 0xb0, 0x2f, 0x5d, + 0x62, 0x87, 0x56, 0x3d, 0xe4, 0x03, 0xae, 0x4a, 0x7e, 0x7d, 0x05, 0xfe, 0x85, 0x33, 0xda, 0x5f, 0x36, 0xcb, 0x56, + 0xa1, 0x14, 0x80, 0x63, 0x26, 0x75, 0x3a, 0xc2, 0x1f, 0x9c, 0xdd, 0x8a, 0xd6, 0xf8, 0xa9, 0x1a, 0xf6, 0xc2, 0x57, + 0xe0, 0x7b, 0x80, 0x55, 0xd5, 0x12, 0xf1, 0xb4, 0xe7, 0x1d, 0x95, 0x68, 0x02, 0xf1, 0x19, 0xff, 0x74, 0x72, 0x32, + 0xdb, 0x6c, 0xc9, 0xa0, 0x2d, 0x69, 0xfc, 0xc0, 0xe8, 0x27, 0x11, 0xb8, 0xe0, 0xf5, 0x83, 0x60, 0x3f, 0xb4, 0x94, + 0xe5, 0x9d, 0xb8, 0xfd, 0xc5, 0x9e, 0x50, 0x76, 0x92, 0xc7, 0x33, 0x6b, 0xe0, 0x7e, 0x9b, 0x2c, 0x3b, 0x27, 0xd8, + 0xa6, 0xda, 0xb3, 0xa1, 0x45, 0xef, 0x3b, 0x3d, 0x76, 0x1d, 0x5a, 0x43, 0xa4, 0xe1, 0x82, 0x5f, 0x7d, 0x0b, 0x10, + 0x28, 0x1c, 0xb9, 0x8f, 0x2b, 0xcd, 0xf9, 0xc5, 0x93, 0xe3, 0x65, 0xa5, 0x5b, 0x50, 0xa9, 0x07, 0x16, 0xb3, 0x45, + 0x4a, 0x69, 0xfc, 0x58, 0x12, 0x7b, 0x58, 0x34, 0x9f, 0x6f, 0x5e, 0x7a, 0xc2, 0x9f, 0x7b, 0xcf, 0x62, 0x62, 0x6c, + 0x1e, 0x7d, 0xd9, 0x72, 0xca, 0x98, 0x1e, 0xd4, 0xe5, 0xc7, 0x98, 0x27, 0xe3, 0x9f, 0xc5, 0x3c, 0x90, 0x9e, 0x26, + 0xed, 0x7e, 0x38, 0x2b, 0xcf, 0xc4, 0x99, 0xf5, 0xb4, 0x78, 0x48, 0x7c, 0x08, 0x99, 0xbc, 0x80, 0x47, 0xb1, 0xc5, + 0x58, 0x60, 0x17, 0x84, 0x11, 0xf8, 0x70, 0xa1, 0x26, 0x95, 0x94, 0x77, 0xf4, 0x8d, 0x1d, 0x47, 0x96, 0xdf, 0x95, + 0x1d, 0x97, 0x37, 0x5a, 0x12, 0xfb, 0x3f, 0xcd, 0xa0, 0xfb, 0xac, 0x61, 0x62, 0x1f, 0xee, 0xdc, 0x6d, 0x2c, 0x45, + 0x5f, 0xc0, 0x80, 0x7d, 0x99, 0x62, 0x38, 0xf7, 0x6d, 0x88, 0xd5, 0xe2, 0x24, 0x9d, 0xed, 0xa7, 0xd5, 0xe7, 0x1c, + 0x8a, 0x75, 0x9b, 0x67, 0x0c, 0x64, 0xfe, 0xe7, 0xe2, 0xe9, 0xa0, 0x94, 0x60, 0x26, 0x46, 0xd8, 0xc9, 0x59, 0x43, + 0x17, 0x85, 0x07, 0x32, 0x8b, 0x0c, 0x5a, 0x1e, 0x35, 0x48, 0x46, 0xe5, 0x44, 0x5b, 0xc7, 0x07, 0xcd, 0x30, 0x97, + 0x80, 0xfa, 0xf9, 0xeb, 0x0d, 0xaf, 0xaf, 0x3e, 0xbe, 0x27, 0x43, 0x8e, 0x4a, 0xf2, 0x60, 0xeb, 0x2b, 0x4d, 0x11, + 0x9a, 0xe1, 0xd7, 0x59, 0x78, 0x29, 0x26, 0x3e, 0x9b, 0xda, 0x61, 0x15, 0xab, 0xe8, 0x6f, 0x81, 0x73, 0xac, 0x9f, + 0x43, 0xc6, 0xb1, 0x81, 0x73, 0x36, 0x4a, 0x1e, 0x57, 0x2b, 0xd9, 0x7a, 0x06, 0x4f, 0xb6, 0x37, 0x11, 0xe6, 0xc6, + 0x90, 0x41, 0xa4, 0xa6, 0xb7, 0x3a, 0x7e, 0xc9, 0xce, 0x50, 0x2f, 0x8c, 0x07, 0xdb, 0x9a, 0x19, 0x38, 0x51, 0x35, + 0x50, 0xf3, 0x09, 0x33, 0x20, 0x62, 0x36, 0xa8, 0x6c, 0x8e, 0xf2, 0x56, 0x8c, 0x82, 0xd3, 0xfa, 0x16, 0xb5, 0x3d, + 0x74, 0x28, 0xdc, 0x2a, 0xe5, 0xae, 0xab, 0x77, 0xe5, 0x22, 0x3d, 0x69, 0x92, 0xb8, 0x56, 0xc5, 0x06, 0x6b, 0x8b, + 0xda, 0x46, 0x18, 0xaf, 0x93, 0xa2, 0x0c, 0xb5, 0xd3, 0xd6, 0x94, 0x4a, 0x29, 0xfd, 0x4b, 0x48, 0x48, 0xa1, 0x73, + 0xbc, 0xde, 0xcb, 0x3a, 0x35, 0x27, 0xd5, 0x40, 0x3a, 0x1d, 0x5b, 0xe3, 0x62, 0xa5, 0x22, 0xa2, 0x7b, 0xb5, 0x6a, + 0xf0, 0x5e, 0x0a, 0x96, 0x5c, 0xe8, 0x3b, 0x41, 0x58, 0xa1, 0xd5, 0x11, 0x2c, 0x36, 0x9e, 0xe5, 0x2b, 0xc9, 0xfb, + 0x1b, 0x37, 0xd0, 0x8e, 0xcf, 0xc2, 0x01, 0x6f, 0xb6, 0x21, 0x96, 0x9c, 0x14, 0x2f, 0x76, 0x19, 0xb0, 0x6a, 0x9d, + 0xef, 0x69, 0xfd, 0xd0, 0x03, 0xd9, 0xeb, 0xb7, 0x86, 0x76, 0x20, 0xd3, 0x20, 0x6a, 0xd8, 0xcb, 0xc8, 0x9e, 0xb7, + 0x82, 0x28, 0xb2, 0x25, 0xa7, 0xa2, 0x6b, 0xfd, 0x60, 0xb0, 0x11, 0x09, 0x53, 0x5f, 0x79, 0x6b, 0x72, 0xd7, 0x1b, + 0x21, 0x73, 0xf7, 0x21, 0xd5, 0x5c, 0xc4, 0xe5, 0x52, 0x82, 0x73, 0x1f, 0x9c, 0x95, 0xe1, 0x21, 0xbf, 0x12, 0x67, + 0xc9, 0x41, 0xe5, 0x64, 0xc4, 0xd4, 0xf9, 0xa2, 0x9b, 0x29, 0xdf, 0xe5, 0xa4, 0xf3, 0xb4, 0x69, 0x6d, 0x2c, 0xb0, + 0x42, 0xe3, 0xe6, 0x25, 0xa7, 0x8f, 0xf5, 0x12, 0x99, 0x8c, 0xbf, 0xbb, 0xd8, 0xa8, 0x23, 0xdb, 0x8e, 0xec, 0x37, + 0xb0, 0x8e, 0x4f, 0xed, 0x67, 0xaa, 0x3e, 0xf6, 0x24, 0x56, 0x96, 0xe3, 0xd4, 0xbc, 0x25, 0x0b, + 0x56, // encrypted new license info + 0xed, 0xe8, 0xbf, 0xd6, 0x13, 0xa0, 0xf5, 0x80, 0x4a, 0xe5, 0xff, 0x85, 0x16, 0xfa, 0xcb, 0x1f, // mac data +]; + +const MAC_DATA: [u8; 16] = [ + 0xed, 0xe8, 0xbf, 0xd6, 0x13, 0xa0, 0xf5, 0x80, 0x4a, 0xe5, 0xff, 0x85, 0x16, 0xfa, 0xcb, 0x1f, +]; + +const NEW_LICENSE_INFORMATION_BUFFER: [u8; 2031] = [ + 0x00, 0x00, 0x06, 0x00, // version + 0x0e, 0x00, 0x00, 0x00, // scope length + 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, // scope + 0x2c, 0x00, 0x00, 0x00, // company name length 26 + 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x20, + 0x00, 0x43, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x70, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, 0x00, + 0x6f, 0x00, 0x6e, 0x00, 0x00, 0x00, // company name 44 + 0x08, 0x00, 0x00, 0x00, // product id length + 0x41, 0x00, 0x30, 0x00, 0x32, 0x00, 0x00, 0x00, // product id + 0x99, 0x07, 0x00, 0x00, // license info length + 0x30, 0x82, 0x07, 0x95, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0, 0x82, 0x07, 0x86, + 0x30, 0x82, 0x07, 0x82, 0x02, 0x01, 0x01, 0x31, 0x00, 0x30, 0x0b, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x07, 0x01, 0xa0, 0x82, 0x07, 0x6a, 0x30, 0x82, 0x02, 0xf1, 0x30, 0x82, 0x01, 0xdd, 0xa0, 0x03, 0x02, 0x01, + 0x02, 0x02, 0x08, 0x01, 0x9e, 0x27, 0x4d, 0x68, 0xac, 0xed, 0x20, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, + 0x1d, 0x05, 0x00, 0x30, 0x32, 0x31, 0x30, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1e, 0x0c, 0x00, 0x52, 0x00, + 0x4f, 0x00, 0x44, 0x00, 0x45, 0x00, 0x4e, 0x00, 0x54, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x07, 0x1e, 0x12, 0x00, + 0x57, 0x00, 0x4f, 0x00, 0x52, 0x00, 0x4b, 0x00, 0x47, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x30, 0x1e, + 0x17, 0x0d, 0x37, 0x30, 0x30, 0x35, 0x33, 0x30, 0x31, 0x30, 0x33, 0x36, 0x31, 0x38, 0x5a, 0x17, 0x0d, 0x34, 0x39, + 0x30, 0x35, 0x33, 0x30, 0x31, 0x30, 0x33, 0x36, 0x31, 0x38, 0x5a, 0x30, 0x32, 0x31, 0x30, 0x30, 0x13, 0x06, 0x03, + 0x55, 0x04, 0x03, 0x1e, 0x0c, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x44, 0x00, 0x45, 0x00, 0x4e, 0x00, 0x54, 0x30, 0x19, + 0x06, 0x03, 0x55, 0x04, 0x07, 0x1e, 0x12, 0x00, 0x57, 0x00, 0x4f, 0x00, 0x52, 0x00, 0x4b, 0x00, 0x47, 0x00, 0x52, + 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, + 0x00, 0x88, 0xad, 0x7c, 0x8f, 0x8b, 0x82, 0x76, 0x5a, 0xbd, 0x8f, 0x6f, 0x62, 0x18, 0xe1, 0xd9, 0xaa, 0x41, 0xfd, + 0xed, 0x68, 0x01, 0xc6, 0x34, 0x35, 0xb0, 0x29, 0x04, 0xca, 0x4a, 0x4a, 0x1c, 0x7e, 0x80, 0x14, 0xf7, 0x8e, 0x77, + 0xb8, 0x25, 0xff, 0x16, 0x47, 0x6f, 0xbd, 0xe2, 0x34, 0x3d, 0x2e, 0x02, 0xb9, 0x53, 0xe4, 0x33, 0x75, 0xad, 0x73, + 0x28, 0x80, 0xa0, 0x4d, 0xfc, 0x6c, 0xc0, 0x22, 0x53, 0x1b, 0x2c, 0xf8, 0xf5, 0x01, 0x60, 0x19, 0x7e, 0x79, 0x19, + 0x39, 0x8d, 0xb5, 0xce, 0x39, 0x58, 0xdd, 0x55, 0x24, 0x3b, 0x55, 0x7b, 0x43, 0xc1, 0x7f, 0x14, 0x2f, 0xb0, 0x64, + 0x3a, 0x54, 0x95, 0x2b, 0x88, 0x49, 0x0c, 0x61, 0x2d, 0xac, 0xf8, 0x45, 0xf5, 0xda, 0x88, 0x18, 0x5f, 0xae, 0x42, + 0xf8, 0x75, 0xc7, 0x26, 0x6d, 0xb5, 0xbb, 0x39, 0x6f, 0xcc, 0x55, 0x1b, 0x32, 0x11, 0x38, 0x8d, 0xe4, 0xe9, 0x44, + 0x84, 0x11, 0x36, 0xa2, 0x61, 0x76, 0xaa, 0x4c, 0xb4, 0xe3, 0x55, 0x0f, 0xe4, 0x77, 0x8e, 0xde, 0xe3, 0xa9, 0xea, + 0xb7, 0x41, 0x94, 0x00, 0x58, 0xaa, 0xc9, 0x34, 0xa2, 0x98, 0xc6, 0x01, 0x1a, 0x76, 0x14, 0x01, 0xa8, 0xdc, 0x30, + 0x7c, 0x77, 0x5a, 0x20, 0x71, 0x5a, 0xa2, 0x3f, 0xaf, 0x13, 0x7e, 0xe8, 0xfd, 0x84, 0xa2, 0x5b, 0xcf, 0x25, 0xe9, + 0xc7, 0x8f, 0xa8, 0xf2, 0x8b, 0x84, 0xc7, 0x04, 0x5e, 0x53, 0x73, 0x4e, 0x0e, 0x89, 0xa3, 0x3c, 0xe7, 0x68, 0x5c, + 0x24, 0xb7, 0x80, 0x53, 0x3c, 0x54, 0xc8, 0xc1, 0x53, 0xaa, 0x71, 0x71, 0x3d, 0x36, 0x15, 0xd6, 0x6a, 0x9d, 0x7d, + 0xde, 0xae, 0xf9, 0xe6, 0xaf, 0x57, 0xae, 0xb9, 0x01, 0x96, 0x5d, 0xe0, 0x4d, 0xcd, 0xed, 0xc8, 0xd7, 0xf3, 0x01, + 0x03, 0x38, 0x10, 0xbe, 0x7c, 0x42, 0x67, 0x01, 0xa7, 0x23, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x13, 0x30, 0x11, + 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x08, 0x30, 0x06, 0x01, 0x01, 0xff, 0x02, 0x01, 0x00, 0x30, 0x09, + 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1d, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x70, 0xdb, 0x21, 0x2b, 0x84, + 0x9a, 0x7a, 0xc3, 0xb1, 0x68, 0xfa, 0xc0, 0x00, 0x8b, 0x71, 0xab, 0x43, 0x9f, 0xb6, 0x7b, 0xb7, 0x1f, 0x20, 0x83, + 0xac, 0x0a, 0xb5, 0x0e, 0xad, 0xb6, 0x36, 0xef, 0x65, 0x17, 0x99, 0x86, 0x8a, 0x3d, 0xba, 0x0c, 0x53, 0x2e, 0xa3, + 0x75, 0xa0, 0xf3, 0x11, 0x3d, 0xe7, 0x65, 0x4b, 0xae, 0x3c, 0x42, 0x70, 0x11, 0xdc, 0xca, 0x83, 0xc0, 0xbe, 0x3e, + 0x97, 0x71, 0x84, 0x69, 0xd6, 0xa8, 0x27, 0x33, 0x9b, 0x3e, 0x17, 0x3c, 0xa0, 0x4c, 0x64, 0xca, 0x20, 0x37, 0xa4, + 0x11, 0xa9, 0x28, 0x8f, 0xb7, 0x18, 0x96, 0x69, 0x15, 0x0d, 0x74, 0x04, 0x75, 0x2a, 0x00, 0xc7, 0xa6, 0x6a, 0xbe, + 0xac, 0xb3, 0xf2, 0xfb, 0x06, 0x1b, 0x6c, 0x11, 0xbd, 0x96, 0xe2, 0x34, 0x74, 0x5d, 0xf5, 0x98, 0x8f, 0x3a, 0x8d, + 0x69, 0x08, 0x6f, 0x53, 0x12, 0x4e, 0x39, 0x80, 0x90, 0xce, 0x8b, 0x5e, 0x88, 0x23, 0x2d, 0xfd, 0x55, 0xfd, 0x58, + 0x3d, 0x39, 0x27, 0xb3, 0x7c, 0x57, 0xfe, 0x3b, 0xab, 0x62, 0x26, 0x60, 0xe2, 0xd0, 0xc8, 0xf4, 0x02, 0x23, 0x16, + 0xc3, 0x52, 0x5d, 0x9f, 0x05, 0x49, 0xa2, 0x71, 0x2d, 0x6d, 0x5b, 0x90, 0xdd, 0xbf, 0xe5, 0xa9, 0x2e, 0xf1, 0x85, + 0x8a, 0x8a, 0xb8, 0xa9, 0x6b, 0x13, 0xcc, 0x8d, 0x4c, 0x22, 0x41, 0xad, 0x32, 0x1e, 0x3b, 0x4b, 0x89, 0x37, 0x66, + 0xdf, 0x1e, 0xa5, 0x4a, 0x03, 0x52, 0x1c, 0xd9, 0x19, 0x79, 0x22, 0xd4, 0xa7, 0x3b, 0x47, 0x93, 0xa9, 0x0c, 0x03, + 0x6a, 0xd8, 0x5f, 0xfc, 0xc0, 0x75, 0x33, 0xe5, 0x26, 0xda, 0xf7, 0x4a, 0x77, 0xd8, 0xf1, 0x30, 0x80, 0x39, 0x38, + 0x1e, 0x86, 0x1d, 0x97, 0x00, 0x9c, 0x0e, 0xba, 0x00, 0x54, 0x8a, 0xc0, 0x12, 0x32, 0x6f, 0x3d, 0xc4, 0x15, 0xf9, + 0x50, 0xf8, 0xce, 0x95, 0x30, 0x82, 0x04, 0x71, 0x30, 0x82, 0x03, 0x5d, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x05, + 0x03, 0x00, 0x00, 0x00, 0x0f, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1d, 0x05, 0x00, 0x30, 0x32, 0x31, + 0x30, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1e, 0x0c, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x44, 0x00, 0x45, 0x00, + 0x4e, 0x00, 0x54, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x07, 0x1e, 0x12, 0x00, 0x57, 0x00, 0x4f, 0x00, 0x52, 0x00, + 0x4b, 0x00, 0x47, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x30, 0x1e, 0x17, 0x0d, 0x30, 0x37, 0x30, 0x36, + 0x32, 0x30, 0x31, 0x34, 0x35, 0x31, 0x33, 0x35, 0x5a, 0x17, 0x0d, 0x30, 0x37, 0x30, 0x39, 0x31, 0x38, 0x31, 0x34, + 0x35, 0x31, 0x33, 0x35, 0x5a, 0x30, 0x7f, 0x31, 0x7d, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1e, 0x0c, 0x00, + 0x52, 0x00, 0x4f, 0x00, 0x44, 0x00, 0x45, 0x00, 0x4e, 0x00, 0x54, 0x30, 0x21, 0x06, 0x03, 0x55, 0x04, 0x07, 0x1e, + 0x1a, 0x00, 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, 0x72, + 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x72, 0x30, 0x43, 0x06, 0x03, 0x55, 0x04, 0x05, 0x1e, 0x3c, 0x00, 0x31, + 0x00, 0x42, 0x00, 0x63, 0x00, 0x4b, 0x00, 0x65, 0x00, 0x64, 0x00, 0x79, 0x00, 0x32, 0x00, 0x6b, 0x00, 0x72, 0x00, + 0x4f, 0x00, 0x34, 0x00, 0x2f, 0x00, 0x4d, 0x00, 0x43, 0x00, 0x44, 0x00, 0x4c, 0x00, 0x49, 0x00, 0x31, 0x00, 0x41, + 0x00, 0x48, 0x00, 0x5a, 0x00, 0x63, 0x00, 0x50, 0x00, 0x69, 0x00, 0x61, 0x00, 0x73, 0x00, 0x3d, 0x00, 0x0d, 0x00, + 0x0a, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, + 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0x88, 0xad, 0x7c, 0x8f, + 0x8b, 0x82, 0x76, 0x5a, 0xbd, 0x8f, 0x6f, 0x62, 0x18, 0xe1, 0xd9, 0xaa, 0x41, 0xfd, 0xed, 0x68, 0x01, 0xc6, 0x34, + 0x35, 0xb0, 0x29, 0x04, 0xca, 0x4a, 0x4a, 0x1c, 0x7e, 0x80, 0x14, 0xf7, 0x8e, 0x77, 0xb8, 0x25, 0xff, 0x16, 0x47, + 0x6f, 0xbd, 0xe2, 0x34, 0x3d, 0x2e, 0x02, 0xb9, 0x53, 0xe4, 0x33, 0x75, 0xad, 0x73, 0x28, 0x80, 0xa0, 0x4d, 0xfc, + 0x6c, 0xc0, 0x22, 0x53, 0x1b, 0x2c, 0xf8, 0xf5, 0x01, 0x60, 0x19, 0x7e, 0x79, 0x19, 0x39, 0x8d, 0xb5, 0xce, 0x39, + 0x58, 0xdd, 0x55, 0x24, 0x3b, 0x55, 0x7b, 0x43, 0xc1, 0x7f, 0x14, 0x2f, 0xb0, 0x64, 0x3a, 0x54, 0x95, 0x2b, 0x88, + 0x49, 0x0c, 0x61, 0x2d, 0xac, 0xf8, 0x45, 0xf5, 0xda, 0x88, 0x18, 0x5f, 0xae, 0x42, 0xf8, 0x75, 0xc7, 0x26, 0x6d, + 0xb5, 0xbb, 0x39, 0x6f, 0xcc, 0x55, 0x1b, 0x32, 0x11, 0x38, 0x8d, 0xe4, 0xe9, 0x44, 0x84, 0x11, 0x36, 0xa2, 0x61, + 0x76, 0xaa, 0x4c, 0xb4, 0xe3, 0x55, 0x0f, 0xe4, 0x77, 0x8e, 0xde, 0xe3, 0xa9, 0xea, 0xb7, 0x41, 0x94, 0x00, 0x58, + 0xaa, 0xc9, 0x34, 0xa2, 0x98, 0xc6, 0x01, 0x1a, 0x76, 0x14, 0x01, 0xa8, 0xdc, 0x30, 0x7c, 0x77, 0x5a, 0x20, 0x71, + 0x5a, 0xa2, 0x3f, 0xaf, 0x13, 0x7e, 0xe8, 0xfd, 0x84, 0xa2, 0x5b, 0xcf, 0x25, 0xe9, 0xc7, 0x8f, 0xa8, 0xf2, 0x8b, + 0x84, 0xc7, 0x04, 0x5e, 0x53, 0x73, 0x4e, 0x0e, 0x89, 0xa3, 0x3c, 0xe7, 0x68, 0x5c, 0x24, 0xb7, 0x80, 0x53, 0x3c, + 0x54, 0xc8, 0xc1, 0x53, 0xaa, 0x71, 0x71, 0x3d, 0x36, 0x15, 0xd6, 0x6a, 0x9d, 0x7d, 0xde, 0xae, 0xf9, 0xe6, 0xaf, + 0x57, 0xae, 0xb9, 0x01, 0x96, 0x5d, 0xe0, 0x4d, 0xcd, 0xed, 0xc8, 0xd7, 0xf3, 0x01, 0x03, 0x38, 0x10, 0xbe, 0x7c, + 0x42, 0x67, 0x01, 0xa7, 0x23, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x82, 0x01, 0x47, 0x30, 0x82, 0x01, 0x43, 0x30, + 0x14, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x04, 0x01, 0x01, 0xff, 0x04, 0x04, 0x01, 0x00, + 0x05, 0x00, 0x30, 0x3c, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x02, 0x01, 0x01, 0xff, 0x04, + 0x2c, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, + 0x20, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x70, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, + 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x30, 0x56, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, + 0x05, 0x01, 0x01, 0xff, 0x04, 0x46, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x1c, 0x00, 0x08, 0x00, 0x24, 0x00, 0x16, 0x00, 0x3a, 0x00, 0x01, 0x00, 0x41, 0x00, 0x30, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x41, 0x00, 0x30, 0x00, 0x32, 0x00, 0x2d, 0x00, 0x36, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x30, + 0x00, 0x2d, 0x00, 0x53, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x80, 0x64, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x6e, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x12, 0x06, 0x01, 0x01, 0xff, 0x04, 0x5e, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x3e, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x44, 0x00, 0x45, 0x00, 0x4e, 0x00, + 0x54, 0x00, 0x00, 0x00, 0x37, 0x00, 0x38, 0x00, 0x34, 0x00, 0x34, 0x00, 0x30, 0x00, 0x2d, 0x00, 0x30, 0x00, 0x30, + 0x00, 0x36, 0x00, 0x2d, 0x00, 0x35, 0x00, 0x38, 0x00, 0x36, 0x00, 0x37, 0x00, 0x30, 0x00, 0x34, 0x00, 0x35, 0x00, + 0x2d, 0x00, 0x37, 0x00, 0x30, 0x00, 0x33, 0x00, 0x34, 0x00, 0x37, 0x00, 0x00, 0x00, 0x57, 0x00, 0x4f, 0x00, 0x52, + 0x00, 0x4b, 0x00, 0x47, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x25, + 0x06, 0x03, 0x55, 0x1d, 0x23, 0x01, 0x01, 0xff, 0x04, 0x1b, 0x30, 0x19, 0xa1, 0x10, 0xa4, 0x0e, 0x52, 0x00, 0x4f, + 0x00, 0x44, 0x00, 0x45, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x00, 0x00, 0x82, 0x05, 0x03, 0x00, 0x00, 0x00, 0x0f, 0x30, + 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1d, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x13, 0x1b, 0xdc, 0x89, + 0xd2, 0xfc, 0x54, 0x0c, 0xee, 0x82, 0x45, 0x68, 0x6a, 0x72, 0xc3, 0x3e, 0x17, 0x73, 0x96, 0x53, 0x44, 0x39, 0x50, + 0x0e, 0x0b, 0x9f, 0x95, 0xd6, 0x2c, 0x6b, 0x53, 0x14, 0x9c, 0xe5, 0x55, 0xed, 0x65, 0xdf, 0x2a, 0xeb, 0x5c, 0x64, + 0x85, 0x70, 0x1f, 0xbc, 0x96, 0xcf, 0xa3, 0x76, 0xb1, 0x72, 0x3b, 0xe1, 0xf6, 0xad, 0xad, 0xad, 0x2a, 0x14, 0xaf, + 0xba, 0xd0, 0xd6, 0xd5, 0x6d, 0x55, 0xec, 0x1e, 0xc3, 0x4b, 0xba, 0x06, 0x9c, 0x59, 0x78, 0x93, 0x64, 0x87, 0x4b, + 0x03, 0xf9, 0xee, 0x4c, 0xdd, 0x36, 0x5b, 0xbd, 0xd4, 0xe5, 0x4c, 0x4e, 0xda, 0x7b, 0xc1, 0xae, 0x23, 0x28, 0x9e, + 0x77, 0x6f, 0x0f, 0xe6, 0x94, 0xfe, 0x05, 0x22, 0x00, 0xab, 0x63, 0x5b, 0xe1, 0x82, 0x45, 0xa6, 0xec, 0x1f, 0x6f, + 0x2c, 0x7b, 0x56, 0xde, 0x78, 0x25, 0x7d, 0x10, 0x60, 0x0e, 0x53, 0x42, 0x4b, 0x6c, 0x7a, 0x6b, 0x5d, 0xc9, 0xd5, + 0xa6, 0xae, 0xc8, 0xc8, 0x52, 0x29, 0xd6, 0x42, 0x56, 0x02, 0xec, 0xf9, 0x23, 0xa8, 0x8c, 0x8d, 0x89, 0xc9, 0x7c, + 0x84, 0x07, 0xfc, 0x33, 0xe1, 0x1e, 0xea, 0xe2, 0x8f, 0x2b, 0xbe, 0x8f, 0xa9, 0xd3, 0xd1, 0xe1, 0x5e, 0x0b, 0xdc, + 0xb6, 0x43, 0x6e, 0x33, 0x0a, 0xf4, 0x2e, 0x9d, 0x0c, 0xc9, 0x58, 0x54, 0x34, 0xaa, 0xe1, 0xd2, 0xa2, 0xe4, 0x90, + 0x02, 0x23, 0x26, 0xa0, 0x92, 0x26, 0x26, 0x0a, 0x83, 0xb4, 0x4d, 0xd9, 0x4b, 0xef, 0xeb, 0x9d, 0xa9, 0x24, 0x3f, + 0x92, 0x8b, 0xdb, 0x04, 0x7b, 0x9d, 0x64, 0x91, 0xa4, 0x4b, 0xd2, 0x6e, 0x51, 0x05, 0x08, 0xc9, 0x91, 0xaf, 0x31, + 0x26, 0x55, 0x21, 0xb1, 0xea, 0xce, 0xa3, 0xa4, 0x0d, 0x5e, 0x4c, 0x46, 0xdb, 0x16, 0x2d, 0x98, 0xdc, 0x60, 0x19, + 0xb8, 0x1b, 0xb9, 0xcd, 0xfb, 0x31, 0x00, // license info +]; + +static NEW_LICENSE_INFORMATION: LazyLock = LazyLock::new(|| LicenseInformation { + version: 0x0006_0000, + scope: "microsoft.com".to_owned(), + company_name: "Microsoft Corporation".to_owned(), + product_id: "A02".to_owned(), + license_info: Vec::from(&NEW_LICENSE_INFORMATION_BUFFER[NEW_LICENSE_INFORMATION_BUFFER.len() - 0x0799..]), +}); +static SERVER_UPGRADE_LICENSE: LazyLock = LazyLock::new(|| { + ServerUpgradeLicense { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::NewLicense, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: u16::try_from(SERVER_UPGRADE_LICENSE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE) + .expect("buffer size is too large"), + }, + encrypted_license_info: Vec::from( + &SERVER_UPGRADE_LICENSE_BUFFER[12..SERVER_UPGRADE_LICENSE_BUFFER.len() - MAC_SIZE], + ), + mac_data: Vec::from(MAC_DATA.as_ref()), + } + .into() +}); + +#[test] +fn from_buffer_correctly_parses_new_license_information() { + assert_eq!( + *NEW_LICENSE_INFORMATION, + decode(NEW_LICENSE_INFORMATION_BUFFER.as_ref()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_new_license_information() { + let serialized_new_license_info = encode_vec(&*NEW_LICENSE_INFORMATION).unwrap(); + + assert_eq!( + NEW_LICENSE_INFORMATION_BUFFER.as_ref(), + serialized_new_license_info.as_slice() + ); +} + +#[test] +fn buffer_length_is_correct_for_new_license_information() { + assert_eq!(NEW_LICENSE_INFORMATION_BUFFER.len(), NEW_LICENSE_INFORMATION.size()); +} + +#[test] +fn from_buffer_correctly_parses_server_upgrade_license() { + assert_eq!( + *SERVER_UPGRADE_LICENSE, + decode(SERVER_UPGRADE_LICENSE_BUFFER.as_ref()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_server_upgrade_license() { + let serialized_upgrade_license = encode_vec(&*SERVER_UPGRADE_LICENSE).unwrap(); + + assert_eq!( + SERVER_UPGRADE_LICENSE_BUFFER.as_ref(), + serialized_upgrade_license.as_slice() + ); +} + +#[test] +fn buffer_length_is_correct_for_server_upgrade_license() { + assert_eq!(SERVER_UPGRADE_LICENSE_BUFFER.len(), SERVER_UPGRADE_LICENSE.size()); +} + +#[test] +fn upgrade_license_verifies_correctly() { + let encrypted_license_info = vec![ + 0xa5, 0x62, 0xcc, 0xe8, 0x5f, 0x22, 0x79, 0x2b, 0xf3, 0xe7, 0x3c, 0x3, 0xde, 0xfe, 0x54, 0x8c, 0xe1, 0xa4, + 0xc2, 0x61, 0x81, 0x8b, 0x48, 0x38, 0x7d, 0x6, 0x4, 0x28, 0xbe, 0x53, 0xc5, 0x30, 0x38, 0x3b, 0x1e, 0xed, 0x48, + 0xc6, 0x2c, 0x88, 0x4, 0xbb, 0x58, 0xd7, 0x10, 0xa8, 0x8e, 0xff, 0x89, 0x4a, 0xc0, 0x3a, 0x6, 0x82, 0xc4, 0xfe, + 0x73, 0x2f, 0x39, 0xe7, 0xb7, 0xba, 0xb7, 0xa8, 0xfb, 0xb6, 0x68, 0x8, 0x90, 0xc6, 0x2d, 0x99, 0xbb, 0x85, + 0x17, 0x48, 0xcd, 0x2d, 0xe1, 0xf7, 0x80, 0x26, 0x78, 0x6d, 0xce, 0x62, 0xed, 0x2, 0x7c, 0x8f, 0x35, 0xde, + 0x1c, 0xb6, 0xa, 0x95, 0xab, 0x38, 0xd9, 0x51, 0x8c, 0x49, 0x35, 0xce, 0xa6, 0xd4, 0x1e, 0x3a, 0xe7, 0xd9, + 0x7e, 0xd4, 0x2a, 0xf9, 0xfa, 0xb2, 0x8, 0xd2, 0xdf, 0xa9, 0x51, 0x2d, 0x6e, 0x8c, 0x99, 0xd2, 0xb6, 0x7c, + 0x30, 0xac, 0xa, 0x8, 0x2f, 0x29, 0xaa, 0x15, 0xfe, 0x8e, 0x3f, 0x99, 0xd, 0x9a, 0xfb, 0x74, 0x68, 0xa9, 0x72, + 0x1, 0xd, 0x5, 0x59, 0x60, 0x8, 0xc3, 0x5b, 0x72, 0xe3, 0x47, 0x71, 0xbe, 0xa7, 0xa4, 0x79, 0x89, 0xde, 0xe2, + 0xd7, 0x5f, 0x97, 0x8e, 0x18, 0x2a, 0xd0, 0x92, 0x30, 0x51, 0xfa, 0xa7, 0xa9, 0x62, 0x7a, 0x32, 0x8e, 0xf, + 0xc5, 0x98, 0xd8, 0x24, 0x39, 0x5a, 0xca, 0xcf, 0x1d, 0x52, 0x62, 0x16, 0x5b, 0xfe, 0x45, 0xfc, 0x56, 0xf0, + 0x20, 0xb1, 0xb4, 0x16, 0x19, 0x1e, 0x19, 0xba, 0x5b, 0xe, 0x73, 0x78, 0xd1, 0x5, 0xe5, 0xa7, 0xbc, 0x6, 0xca, + 0x3b, 0x3b, 0x16, 0x48, 0xa4, 0x52, 0x58, 0x20, 0x8, 0x91, 0x31, 0x17, 0x68, 0xd1, 0x2a, 0x88, 0x5b, 0xd2, + 0xc1, 0xd3, 0xd0, 0x88, 0xc5, 0x2c, 0xfa, 0xdc, 0x39, 0xf7, 0xeb, 0x72, 0x6d, 0x6b, 0x77, 0xd7, 0x57, 0x6f, + 0x62, 0x40, 0x12, 0xc0, 0xff, 0xb2, 0xaa, 0xbf, 0x35, 0x11, 0x30, 0xc7, 0xf4, 0x78, 0xf8, 0x70, 0x40, 0xf0, + 0xb6, 0x20, 0xab, 0xb, 0x94, 0x52, 0xd5, 0x53, 0x74, 0x3b, 0x4b, 0xfb, 0x53, 0x83, 0xf1, 0x7f, 0x46, 0xef, + 0x37, 0xe0, 0x44, 0xba, 0xc, 0x5a, 0x65, 0x7d, 0x37, 0x1f, 0x66, 0xe, 0x6f, 0x75, 0x1a, 0x2d, 0xe9, 0xd6, 0x3f, + 0x5, 0x74, 0xa9, 0x7, 0x5, 0xc4, 0x4f, 0x6a, 0x8d, 0x5a, 0xad, 0x95, 0xf5, 0xf2, 0x2a, 0xd9, 0x32, 0x4c, 0x4d, + 0x44, 0x6a, 0xb4, 0x1e, 0xb7, 0x29, 0x2, 0xf4, 0x2f, 0x23, 0x52, 0x8a, 0xe6, 0xb0, 0x94, 0x29, 0x7c, 0xf8, + 0x9e, 0x99, 0xbe, 0x63, 0x32, 0xa9, 0x2c, 0xdf, 0xb7, 0x64, 0xe5, 0xc5, 0xa7, 0x24, 0x75, 0xb1, 0x43, 0x6f, + 0x7, 0xf3, 0x7e, 0x15, 0x9a, 0x8f, 0x5e, 0xc0, 0x15, 0x8f, 0xcc, 0xfa, 0x3c, 0x9e, 0x66, 0xa, 0x84, 0xe4, 0x70, + 0xb2, 0x20, 0x89, 0x4e, 0x1e, 0x29, 0xbf, 0x74, 0x7f, 0xd1, 0x48, 0x64, 0x1d, 0xec, 0x3, 0xe9, 0x85, 0x76, + 0xab, 0xcf, 0x93, 0xfa, 0x6f, 0x4a, 0xf5, 0xa4, 0xc5, 0xf5, 0xc3, 0x6f, 0xd5, 0xae, 0x58, 0xa9, 0x94, 0x77, + 0x64, 0x75, 0x45, 0x12, 0x7d, 0x7a, 0x6d, 0x83, 0x2b, 0x23, 0xf9, 0x3e, 0x5e, 0x49, 0x57, 0xbe, 0x4f, 0x64, + 0x59, 0xcc, 0xae, 0x8f, 0xea, 0x7b, 0xa6, 0xe3, 0x6b, 0x8e, 0xab, 0xa, 0xdf, 0x59, 0x8b, 0x5, 0x9b, 0x93, 0x32, + 0xe8, 0x2e, 0x6b, 0xcc, 0xcb, 0x2e, 0xb8, 0x26, 0x26, 0x1b, 0x42, 0x8c, 0xf3, 0xe2, 0xaf, 0xf9, 0xb3, 0xfe, + 0x49, 0xf6, 0x8b, 0x9b, 0xea, 0x64, 0xc2, 0x85, 0xca, 0x59, 0x5a, 0xfe, 0xe5, 0xc5, 0xd4, 0x7a, 0x0, 0x87, + 0x4a, 0xb5, 0xe8, 0xb4, 0xbb, 0x6a, 0x84, 0x87, 0x7e, 0x82, 0x44, 0x13, 0xcf, 0xf, 0xeb, 0xff, 0xd7, 0xf0, + 0xab, 0x56, 0x79, 0xa1, 0x33, 0xe, 0xf0, 0xa, 0x4b, 0x1, 0x81, 0x8c, 0xfe, 0x76, 0xb2, 0xd1, 0x57, 0x3d, 0x26, + 0x94, 0x36, 0x19, 0x74, 0x48, 0x48, 0xcd, 0xc3, 0xfc, 0xc0, 0xe5, 0xb6, 0x2b, 0xef, 0xb2, 0x6a, 0x44, 0x64, + 0x54, 0x42, 0x6e, 0x10, 0xf0, 0xfe, 0x6, 0xbe, 0x1, 0x73, 0xde, 0x16, 0x2e, 0x4b, 0xa9, 0x5b, 0xe0, 0xa9, 0x1a, + 0xda, 0xb9, 0xb3, 0x7e, 0xaf, 0x63, 0x1, 0x6, 0x9a, 0x8, 0xce, 0x7e, 0x3a, 0x3f, 0x33, 0xb9, 0x41, 0x38, 0xaf, + 0x43, 0xa, 0x79, 0xe3, 0xf, 0x7c, 0x9a, 0x6c, 0xbd, 0x2c, 0xc3, 0xa2, 0xba, 0x50, 0x98, 0xaa, 0x77, 0xc4, 0x2, + 0x97, 0xc7, 0x60, 0xc4, 0x4d, 0x14, 0xf9, 0xa0, 0xe0, 0x96, 0xa6, 0x27, 0x83, 0xbd, 0x96, 0x1b, 0xf6, 0xcf, + 0x9f, 0xba, 0xe8, 0xac, 0x38, 0x4d, 0xf9, 0xe0, 0x49, 0x75, 0xd1, 0xf8, 0x5a, 0xff, 0x43, 0x87, 0x5, 0x45, + 0x18, 0x90, 0x8f, 0x53, 0xf8, 0x8e, 0xf7, 0x3b, 0x2b, 0xfe, 0x2f, 0xcf, 0x5d, 0x73, 0x59, 0xd8, 0x8f, 0xc7, + 0x3a, 0x4f, 0xa7, 0x78, 0xfa, 0xc4, 0x68, 0xd, 0x95, 0xc7, 0x82, 0x6b, 0xa2, 0xb7, 0x1f, 0xdc, 0xb8, 0xc9, + 0x5d, 0x83, 0x27, 0x24, 0x84, 0x2f, 0xf4, 0x5f, 0xb, 0xa7, 0x3d, 0x0, 0x35, 0xbb, 0x5c, 0xbb, 0xbf, 0x5e, 0x7c, + 0xb4, 0xf4, 0x43, 0x13, 0x2b, 0x17, 0x6e, 0xef, 0xbf, 0x19, 0x57, 0xa4, 0x29, 0xbc, 0xdf, 0xd9, 0x89, 0x7b, + 0x2d, 0x54, 0x46, 0xd6, 0x44, 0x90, 0x37, 0xea, 0xeb, 0x74, 0xb6, 0x9f, 0xe5, 0x4e, 0x57, 0x63, 0x43, 0xe5, + 0xd7, 0xf, 0x72, 0x65, 0x8c, 0x91, 0xe, 0xd4, 0xdd, 0x67, 0xce, 0x6a, 0xb5, 0xe2, 0x60, 0x38, 0x5a, 0xe8, 0x3b, + 0x55, 0xd3, 0x9a, 0x12, 0xdb, 0x57, 0x3, 0xb3, 0x64, 0xd9, 0x96, 0x9b, 0x7, 0xfd, 0x25, 0x41, 0x8c, 0xbc, 0x20, + 0x91, 0x11, 0x1, 0x5f, 0xfe, 0x44, 0x1e, 0x68, 0xc, 0x40, 0xa4, 0xc3, 0xc2, 0xb7, 0x4d, 0x5c, 0xa1, 0x36, 0x6e, + 0x52, 0xda, 0xed, 0x71, 0xc5, 0xce, 0x27, 0x1e, 0xdc, 0xfd, 0xb8, 0xda, 0x70, 0xba, 0x7a, 0x33, 0x66, 0x91, + 0x62, 0x46, 0xd1, 0xae, 0x12, 0xc8, 0x90, 0xe4, 0xb4, 0x21, 0x2f, 0xa5, 0xb8, 0x6, 0x71, 0x26, 0x49, 0x37, + 0xe5, 0xc7, 0xd6, 0xa1, 0x4f, 0xb0, 0xe9, 0xdb, 0xce, 0xee, 0x6e, 0xc1, 0xb, 0x11, 0x5f, 0x6f, 0x72, 0x23, + 0xb5, 0xb6, 0x81, 0x4d, 0x7f, 0xe3, 0xe5, 0x41, 0xf4, 0x29, 0x55, 0x89, 0xfd, 0x62, 0x5d, 0x11, 0xaa, 0xd, + 0x94, 0x58, 0xe0, 0x9f, 0xd9, 0xb7, 0xc7, 0x47, 0x4e, 0x70, 0xe6, 0x84, 0xb1, 0x5d, 0x3a, 0xd1, 0xed, 0x5, + 0x61, 0x79, 0xe2, 0xe7, 0x5, 0x53, 0x94, 0xe2, 0x44, 0x49, 0xc, 0xc4, 0x36, 0x85, 0x22, 0xb9, 0x7b, 0xa5, 0xb, + 0x59, 0xeb, 0xb8, 0x5d, 0x38, 0x4d, 0x6d, 0xe, 0x6b, 0x8a, 0x5f, 0x9f, 0x3b, 0x93, 0x5f, 0x26, 0xc3, 0x33, + 0x17, 0x21, 0x15, 0x3c, 0x92, 0xf3, 0x53, 0x48, 0x1c, 0x2c, 0x46, 0x48, 0xee, 0x9e, 0xf1, 0x9d, 0x12, 0x0, + 0xd2, 0x7c, 0x7b, 0xd1, 0xd5, 0x7c, 0xdc, 0x63, 0x2e, 0x22, 0x1e, 0x16, 0x8a, 0x31, 0x3, 0xee, 0x24, 0x11, + 0x53, 0x15, 0xef, 0x15, 0xf1, 0xcd, 0xc, 0x33, 0x51, 0x20, 0xab, 0xab, 0xa3, 0x6d, 0xdb, 0x89, 0x12, 0xfe, + 0xeb, 0x59, 0x75, 0x84, 0x28, 0xf9, 0x62, 0x86, 0xfb, 0xd9, 0xca, 0xc6, 0x28, 0x88, 0x1b, 0x41, 0xfe, 0xf5, + 0xe4, 0x6c, 0x15, 0x1a, 0xa1, 0xe1, 0xe3, 0x9, 0xb0, 0x71, 0x16, 0xc2, 0xee, 0xb4, 0x54, 0x48, 0x9d, 0x37, + 0xa1, 0x33, 0x86, 0xdd, 0x3f, 0x6e, 0x9b, 0x3f, 0x99, 0xce, 0x11, 0x5c, 0xea, 0xc9, 0xec, 0xaa, 0x17, 0xd6, + 0x4d, 0x5c, 0x77, 0x99, 0x5, 0x23, 0xd4, 0x1d, 0xa1, 0xc1, 0x8c, 0xec, 0x64, 0x90, 0xd5, 0xb3, 0xb8, 0x73, + 0x88, 0x0, 0xe6, 0x41, 0x88, 0x2b, 0x77, 0x9, 0x3, 0xa0, 0xe4, 0xa4, 0x93, 0xe7, 0xbe, 0xd7, 0xe9, 0x6c, 0x0, + 0x15, 0xa6, 0xde, 0x38, 0xda, 0x2b, 0x6d, 0xfb, 0xde, 0x0, 0x96, 0x75, 0x8e, 0x8c, 0xef, 0x94, 0xc4, 0x64, + 0xcd, 0xfc, 0xb7, 0xf8, 0x6, 0x5c, 0xcc, 0x41, 0xf6, 0xce, 0x2c, 0xf3, 0xba, 0x44, 0x9e, 0x9f, 0xa8, 0xe6, + 0xb3, 0x5a, 0xdb, 0x38, 0x5e, 0x53, 0x45, 0x4, 0xb2, 0x3e, 0xe7, 0xea, 0xc5, 0x1b, 0x40, 0xaf, 0xd0, 0xb7, + 0xd3, 0x6c, 0x9e, 0x30, 0xa1, 0x72, 0xc, 0xde, 0x66, 0x3b, 0x9c, 0x75, 0xb7, 0x67, 0x48, 0x17, 0x8c, 0x97, + 0xbc, 0xd2, 0x9, 0xc6, 0x94, 0xc9, 0x3f, 0xbf, 0xd6, 0x1d, 0x5f, 0x63, 0x18, 0xf1, 0x95, 0x6c, 0x49, 0x37, + 0x85, 0xb0, 0x3f, 0xa4, 0x7a, 0x36, 0xa8, 0xeb, 0xe1, 0xce, 0x88, 0xcc, 0x37, 0x45, 0xa9, 0xda, 0x91, 0x96, + 0xfa, 0xc4, 0xda, 0x2e, 0xcc, 0x49, 0xe1, 0x98, 0xf1, 0x93, 0x28, 0x37, 0xa0, 0xf4, 0x5f, 0xda, 0x50, 0xfc, + 0x63, 0xef, 0x74, 0x29, 0xc0, 0x31, 0xfb, 0x6a, 0x9a, 0x19, 0x4f, 0x44, 0x54, 0xcf, 0x8c, 0x77, 0xda, 0xf3, + 0xaa, 0xe7, 0x1f, 0x5, 0xa1, 0x11, 0xf1, 0x1b, 0xa6, 0x77, 0xbb, 0xc6, 0xa8, 0x59, 0x2c, 0x9d, 0xea, 0x98, + 0x1a, 0x7c, 0x65, 0x79, 0x88, 0xdb, 0x30, 0xa2, 0xe8, 0xa, 0xe1, 0x21, 0x82, 0x2f, 0x68, 0xb0, 0xe4, 0xce, + 0xe7, 0x46, 0x82, 0x8d, 0xb5, 0xea, 0xad, 0x26, 0xb6, 0xf4, 0xd6, 0xdd, 0x6b, 0xd3, 0x30, 0x84, 0x2e, 0xd8, + 0x72, 0x1, 0xec, 0x68, 0x49, 0x4f, 0xa2, 0x3f, 0xe1, 0xee, 0x38, 0x59, 0x69, 0x2f, 0xb, 0x92, 0x14, 0xb9, 0xc5, + 0x3b, 0xae, 0x0, 0xf5, 0x23, 0xd5, 0x3c, 0x1b, 0x9f, 0xef, 0xe1, 0x4c, 0x40, 0xfd, 0x8, 0xba, 0x81, 0xbc, 0x3c, + 0x6d, 0x59, 0x92, 0x4a, 0x5c, 0xa8, 0xc5, 0xfd, 0x7b, 0x71, 0xa5, 0x13, 0xfb, 0xdd, 0x15, 0x99, 0x21, 0x42, + 0x54, 0x66, 0x78, 0xc5, 0x75, 0xac, 0xfb, 0xca, 0x71, 0x91, 0xae, 0x63, 0xf1, 0x67, 0x29, 0xc, 0x4b, 0xaf, + 0x78, 0xad, 0xd9, 0xc1, 0x9, 0xa7, 0x28, 0x14, 0x78, 0x50, 0xe3, 0x45, 0xfa, 0xb7, 0x41, 0x9f, 0x96, 0xd4, + 0x51, 0x56, 0xf7, 0xee, 0xdf, 0xf8, 0xa5, 0x36, 0x85, 0xa0, 0x72, 0xc9, 0x64, 0xb0, 0x23, 0x1d, 0xd6, 0x62, + 0x7, 0xf3, 0xf1, 0xfb, 0x41, 0x17, 0x32, 0xef, 0x56, 0x6d, 0x83, 0xc4, 0x1f, 0x70, 0x8b, 0x73, 0x33, 0x62, + 0xba, 0x47, 0x1, 0x24, 0x89, 0xed, 0xc, 0xc0, 0x18, 0x9, 0x40, 0x93, 0xda, 0x5e, 0x39, 0x2f, 0xd4, 0x3f, 0x9d, + 0xb3, 0xa4, 0xd, 0xd1, 0x3b, 0xc4, 0xf4, 0xf7, 0x23, 0xa5, 0xb, 0x9a, 0x93, 0xae, 0xc9, 0x3, 0xf, 0x9b, 0xbb, + 0xb0, 0xc2, 0xb0, 0x41, 0x6e, 0xaf, 0xf2, 0xb9, 0xad, 0x9a, 0x71, 0xc1, 0x87, 0xf0, 0x0, 0x53, 0xac, 0xad, + 0x24, 0x4c, 0x6f, 0x14, 0xd2, 0xd1, 0x85, 0x56, 0xb2, 0x7c, 0xf9, 0xa2, 0x92, 0x81, 0x43, 0x8e, 0xc8, 0xa4, + 0x58, 0x79, 0x4a, 0xde, 0x8f, 0xac, 0xa1, 0xcb, 0xf2, 0x9c, 0x38, 0x9c, 0xd6, 0xb2, 0x1a, 0xfa, 0x1c, 0xb5, + 0x19, 0xe2, 0x13, 0x36, 0x41, 0x9e, 0xc3, 0x73, 0xe9, 0x20, 0xf, 0xe3, 0xfd, 0xc, 0xa2, 0xd1, 0x77, 0xf8, 0xae, + 0xb6, 0x11, 0xd1, 0x55, 0x5b, 0xae, 0xd9, 0xb5, 0x2a, 0xd3, 0x96, 0x86, 0x77, 0xf7, 0xe2, 0x2, 0xa6, 0xc2, + 0x10, 0x37, 0x2f, 0x10, 0xff, 0xee, 0xc9, 0x29, 0xd1, 0xf4, 0x89, 0x4b, 0x89, 0x0, 0xee, 0xb5, 0x9f, 0xd1, + 0xad, 0x7b, 0x92, 0x17, 0x87, 0x85, 0x21, 0x59, 0xc7, 0xc3, 0xbf, 0x86, 0xf6, 0xff, 0xd8, 0x8a, 0x47, 0xb9, + 0x61, 0xa1, 0x52, 0xed, 0xf, 0x34, 0x17, 0xe2, 0xcb, 0x92, 0x0, 0x16, 0x2a, 0x1f, 0x4a, 0x60, 0x54, 0xd7, 0x1d, + 0x7a, 0x97, 0x4c, 0x91, 0x41, 0x8f, 0x80, 0x68, 0xbd, 0xa5, 0x57, 0x26, 0xe4, 0xf7, 0x8a, 0xe0, 0x57, 0x76, + 0x58, 0xe, 0x14, 0xfc, 0xab, 0xae, 0x60, 0xa8, 0xb2, 0x62, 0xd0, 0x9, 0x4b, 0xbc, 0x8, 0x20, 0x1f, 0x58, 0x9, + 0x52, 0x8, 0xd4, 0xf5, 0xdb, 0x87, 0x1e, 0x1c, 0xaf, 0x47, 0xe, 0x8b, 0x19, 0x3f, 0xf3, 0x7, 0x38, 0x5a, 0x5c, + 0x29, 0x66, 0x54, 0x77, 0x22, 0x4f, 0xb6, 0x5d, 0xc6, 0xc9, 0x35, 0x44, 0xe9, 0xeb, 0x33, 0x8a, 0x12, 0x77, + 0xc2, 0x1e, 0x83, 0xca, 0xfa, 0x1a, 0x2f, 0xef, 0x13, 0x6f, 0x70, 0xa6, 0x1f, 0x2e, 0x61, 0x83, 0xf4, 0x10, + 0xb8, 0x1, 0xa4, 0xb7, 0x9, 0xaa, 0x32, 0x36, 0x77, 0x39, 0x62, 0xfa, 0x7b, 0x19, 0x6b, 0x97, 0xe0, 0xd2, 0x1, + 0xfd, 0x33, 0x60, 0x8, 0xe0, 0x4, 0xdf, 0xa7, 0x86, 0xd1, 0x8d, 0xc4, 0xf6, 0x9c, 0xed, 0x77, 0x68, 0x9d, 0xee, + 0xd0, 0x67, 0xc7, 0xce, 0xd7, 0xf2, 0x2b, 0x2f, 0xe2, 0x82, 0x80, 0x89, 0x4b, 0x4c, 0x35, 0xd8, 0xf3, 0xca, + 0x8a, 0xd5, 0x37, 0xe, 0xf4, 0xac, 0x2f, 0xd7, 0xc2, 0x34, 0x39, 0x81, 0xce, 0x95, 0x65, 0x11, 0x2, 0xfa, 0xa0, + 0x20, 0x96, 0xd2, 0x7b, 0x42, 0x6b, 0xc8, 0x91, 0x9d, 0xe8, 0xc7, 0x62, 0xc7, 0x32, 0x6b, 0x66, 0x59, 0xe1, + 0xca, 0xf7, 0x42, 0x4a, 0x6c, 0xca, 0x72, 0xe, 0x82, 0x3, 0x93, 0xd6, 0xdf, 0xa5, 0xd3, 0xf6, 0xa6, 0x62, 0xec, + 0x85, 0x12, 0x7a, 0xde, 0x1, 0xd5, 0x13, 0x47, 0x23, 0x3e, 0xda, 0xcb, 0x3, 0x72, 0xde, 0x5e, 0x70, 0x10, 0x52, + 0x7e, 0x62, 0x84, 0x1e, 0x66, 0x37, 0x3a, 0x48, 0x5d, 0x96, 0x2b, 0x8f, 0x1, 0x16, 0xb, 0x0, 0xff, 0x1d, 0xf7, + 0xe8, 0xa2, 0xc7, 0xa1, 0xc9, 0x39, 0x8b, 0x75, 0x59, 0x8d, 0x33, 0x5e, 0x54, 0xae, 0xa8, 0x2b, 0x63, 0x89, + 0x5e, 0x7a, 0x3e, 0x35, 0x5c, 0x39, 0x9c, 0xfc, 0xa8, 0x1b, 0x33, 0x4b, 0xfe, 0xf3, 0xa7, 0x2, 0xe5, 0xc3, + 0x6b, 0x17, 0xc1, 0xb5, 0xdb, 0x26, 0x51, 0xff, 0x58, 0x4a, 0x3, 0x52, 0x1e, 0xde, 0x8, 0x96, 0x3a, 0x7d, 0x14, + 0x63, 0x80, 0xca, 0x5, 0x7f, 0x24, 0x73, 0x65, 0xb7, 0x70, 0x5a, 0x4f, 0xea, 0x6f, 0x8d, 0xa6, 0xf0, 0x6f, + 0x60, 0xdb, 0x34, 0x77, 0xef, 0xa0, 0x21, 0x17, 0xa2, 0x95, 0x4a, 0x73, 0xad, 0x68, 0xc1, 0xf3, 0xad, 0x67, + 0x68, 0x30, 0xa0, 0xda, 0x98, 0xfb, 0x15, 0xd7, 0x28, 0x9b, 0xb3, 0xf0, 0x34, 0x92, 0xb5, 0x3, 0x12, 0x0, 0xcb, + 0x54, 0x98, 0xf2, 0x75, 0x1d, 0x7a, 0x38, 0x5d, 0x3, 0xb1, 0xa9, 0x70, 0xc7, 0x5a, 0x1d, 0x25, 0x73, 0xfc, + 0xfb, 0x13, 0x6d, 0x5b, 0xe9, 0xb4, 0x5, 0xd2, 0xd, 0x1a, 0xd2, 0x38, 0x1c, 0x75, 0x19, 0xa, 0x9b, 0x16, 0x9, + 0xfb, 0x1a, 0xb2, 0xe2, 0xe, 0x85, 0x6e, 0x32, 0x9, 0x1b, 0xb5, 0xc3, 0x3b, 0x78, 0x41, 0xf0, 0x92, 0xeb, 0xcd, + 0x9, 0xde, 0x61, 0x30, 0x1f, 0xe2, 0x24, 0x80, 0x50, 0xa5, 0xe, 0x9, 0x18, 0xe, 0xe5, 0x70, 0x1f, 0xde, 0x6c, + 0x51, 0x3e, 0xdd, 0x9e, 0xad, 0xc5, 0x79, 0xd3, 0x2e, 0x9, 0x7, 0xa7, 0x52, 0xb7, 0x2c, 0x7f, 0xdb, 0x66, 0xf8, + 0xa2, 0xea, 0x47, 0xfe, 0xe2, 0x75, 0x77, 0xb3, 0xa9, 0x69, 0x98, 0xa3, 0x1a, 0xd9, 0xec, 0x5e, 0xf6, 0x2b, + 0x58, 0x8c, 0xd0, 0x58, 0x3f, 0x74, 0x7e, 0x6f, 0xe2, 0xe3, 0x6f, 0xa8, 0xc8, 0x1a, 0xf7, 0x7f, 0xbb, 0x7, + 0xca, 0x3, 0xfa, 0xab, 0xd8, 0x28, 0x15, 0x90, 0x89, 0x89, 0xf, 0xc4, 0xe2, 0xf8, 0xa6, 0xb7, 0x92, 0x59, 0x24, + 0x53, 0x15, 0x25, 0xd5, 0xf2, 0xb, 0xc2, 0xf0, 0x81, 0x16, 0x39, 0xcb, 0x7b, 0xae, 0x1e, 0xc1, 0x5d, 0x16, + 0xb8, 0x6d, 0x30, 0xdc, 0xf0, 0x17, 0x99, 0x8d, 0xd0, 0xab, 0x2a, 0xc8, 0x24, 0x3a, 0xd7, 0xe4, 0x16, 0x50, + 0xb7, 0x36, 0x6, 0x99, 0x90, 0x91, 0x84, 0xad, 0xf1, 0xb, 0xc2, 0xec, 0x15, 0x3a, 0x6e, 0xe8, 0x6, 0x5e, 0xcf, + 0x1a, 0x82, 0x15, 0xd8, 0x83, 0x60, 0x4c, 0xe8, 0x2d, 0x68, 0x7b, 0x6, 0x87, 0xd0, 0xe2, 0xec, 0x1f, 0x56, + 0xb1, 0xc7, 0xa8, 0xad, 0x7e, 0xf2, 0x25, 0x59, 0xbb, 0x2c, 0xc0, 0x2e, 0x56, 0x68, 0x2c, 0xa0, 0xd8, 0x27, + 0xd5, 0x9d, 0xb1, 0x9c, 0x3, 0xa3, 0xa7, 0xb8, 0xde, 0x5d, 0x62, 0xe, 0x8b, 0xb6, 0x7a, 0xf2, 0x8, 0x8, 0x51, + 0xe8, 0xe3, 0xa4, 0x13, 0x20, 0xcf, 0xa7, 0xbf, 0x3c, 0x62, 0xbc, 0xad, 0x16, 0xba, 0x84, 0xb8, 0xab, 0xac, + 0x15, 0x8e, 0x29, 0x33, 0x12, 0xd0, 0x83, 0x21, 0x5f, 0x87, 0x5a, 0x2f, 0x74, 0xd4, 0x91, 0xf8, 0x3f, 0x6d, + 0xa9, 0xa5, 0x62, 0xaa, 0x47, 0x62, 0x58, 0xf1, 0x16, 0x36, 0x81, 0x38, 0xc2, 0x21, 0xba, 0x23, 0x1, 0x27, + 0x1f, 0xad, 0x7f, 0x4a, 0x3e, 0xca, 0x9d, 0x9e, 0x4e, 0xea, 0xeb, 0xa4, 0xd7, 0xd, 0xf3, 0xa0, 0x12, 0x64, + 0xdc, 0xf4, 0x4e, 0xa7, 0x45, 0x55, 0x97, 0xef, 0x75, 0x78, 0x91, 0x6e, 0x66, 0x72, 0xe4, 0x84, 0xe5, 0xc4, + 0x72, 0xc4, 0xc0, 0x3, 0x83, 0xd4, 0x31, 0xc4, 0x49, 0x43, 0xe1, 0x79, 0x7a, 0x70, 0xe2, 0x40, 0xa1, 0x51, + 0x56, 0xae, 0x11, 0xfb, 0x7b, 0x89, 0x8a, 0xdd, 0x55, 0xa4, 0x18, 0xb6, 0xee, 0x6, 0xd7, 0x8a, 0xc1, 0x89, + 0xbf, 0x2d, 0xd7, 0xbf, 0x18, 0x14, 0xb0, 0xec, 0xed, 0xe1, 0x20, 0x18, 0xb6, 0x21, 0x6b, 0x3c, 0xe0, 0x9f, + 0x92, 0xed, 0xb4, 0x40, 0xd9, 0x77, 0x32, 0x50, 0x85, 0x25, 0x39, 0x2c, 0x37, 0x31, 0x4c, 0x9e, 0xbf, 0xe4, + 0xfc, 0x47, 0xe7, 0xc4, 0xaf, 0x4d, 0x7e, 0x9d, 0x80, 0x34, 0x60, 0xc5, 0xca, 0x74, 0xef, 0x38, 0x4, 0x71, + 0xf3, 0x1b, 0x99, 0x3f, 0xb5, 0xa8, 0x10, 0xf, 0xef, 0x84, 0x1, 0xe1, 0x9, 0x46, 0xe7, 0xe2, 0x56, 0x19, 0x7d, + 0x6a, 0x9f, 0x21, 0x7a, 0xd1, 0x82, 0x59, 0x7c, 0x9a, 0x13, 0xe2, 0x7f, 0x1e, 0x71, 0xcb, 0xf5, 0xf6, 0x47, + 0x8, 0x30, 0xa3, 0x3e, 0x1f, 0x2e, 0x26, 0x80, 0xe6, 0x1a, 0x62, 0x24, 0x1f, 0x18, 0x11, 0xf, 0xcf, 0xa7, 0x76, + 0x57, 0x3f, 0x7, 0xba, 0xbc, 0x26, 0xdc, 0x49, 0x38, 0x53, 0x71, 0xdc, 0x5b, 0xc3, 0x9f, 0x17, 0x44, 0xe9, + 0x58, 0xde, 0x97, 0xbf, 0x5a, 0xf, 0x69, 0x57, 0xb7, 0x3b, 0xda, 0x9c, 0x6c, 0x3a, 0x9c, 0x47, 0x2d, 0xd9, + 0xc3, 0x3a, 0xbf, 0x4c, 0x10, 0x75, 0x33, 0x3b, 0x83, 0x28, 0x70, 0xc7, 0xd7, 0x6e, 0x2f, 0xeb, 0xd3, 0xe5, + 0x39, 0x20, 0x8f, 0x6f, 0x9f, 0xad, 0x19, 0xe3, 0x3f, 0x40, 0xb0, 0x14, 0xef, 0x9c, 0xfd, 0x91, 0xf3, 0xf7, + 0x7f, 0x94, 0x47, 0xbb, 0x22, 0xc7, 0x68, 0xc9, 0xf1, 0x4b, 0x4b, 0x46, 0x2c, 0xa2, 0x6f, 0xd3, 0xc0, 0x48, + 0xef, 0xde, 0xde, 0xc4, 0x55, 0x8f, 0xfd, 0xff, 0x12, 0xd0, 0x38, 0xc0, 0x4, 0xe9, 0x1b, 0x73, 0x5, 0xe0, 0x10, + 0x53, 0xd7, 0x4b, 0x84, 0x80, 0xab, 0xea, 0xf5, 0xd9, 0xb4, 0x6f, 0xbf, 0xea, 0x25, 0x90, 0xcf, 0x4f, 0x15, + 0xc4, 0xd3, 0xa3, 0x83, 0x59, 0xf, 0x38, 0x66, 0xfa, 0xb8, 0xec, 0xde, 0xdc, 0x31, 0x47, 0xbf, 0x8b, 0xfe, + 0x27, 0x28, 0x70, 0x79, 0xf6, 0x44, 0x7f, 0xcb, 0xf7, 0xab, 0x55, 0x56, 0x9b, 0xd4, 0x9a, 0xbf, 0xa3, 0xa9, + 0xbd, 0xc2, 0xb8, 0x1c, 0x85, 0x3c, 0x13, 0x5e, 0xd7, 0x97, 0xec, 0xf9, 0x94, 0x5f, 0xf8, 0xa0, 0xb9, 0x4d, + 0x2, 0x8e, 0x9f, 0x64, 0xbe, 0x69, 0xb0, 0x94, 0x92, 0xbd, 0xd8, 0xfa, 0xc9, 0x7, 0x29, 0x7a, 0x0, 0x2f, 0xe8, + 0x9f, 0x33, 0xb4, 0xcb, 0x3f, 0xd7, 0x6c, 0x65, 0xf8, 0x15, 0xa7, 0xa7, 0x5, 0x4a, 0xb7, 0x6c, 0x28, 0x5c, + 0xf3, 0x3, 0xb1, 0x80, 0x93, 0x86, 0xf9, 0xdf, 0x4b, 0x56, 0x96, 0x36, 0xfd, 0x36, 0xc9, 0xe4, 0xa2, 0x24, + 0x34, 0x40, 0x54, 0x35, 0xbe, 0x26, 0x94, 0xd0, 0xd4, 0xd4, 0x55, 0xb4, 0x47, 0xf8, 0xe4, 0x7c, 0xe5, 0x37, + 0xdf, 0x11, 0xba, 0xea, 0xd, 0x92, 0xed, 0x92, 0x93, 0x7c, 0xa0, 0xad, 0x89, 0xd4, 0x82, 0xc5, 0x74, 0xfc, + 0x56, 0x69, 0x24, 0xf8, 0xe6, 0xfa, 0xc2, 0xd9, 0x7, 0x47, 0xb8, 0x4e, 0x9e, 0x23, 0x13, 0x16, 0xe0, 0xa1, + 0xcd, 0x2f, 0x69, 0x62, 0x4c, 0xc0, 0x72, 0x22, 0x86, 0x2f, 0x3a, 0x1c, 0xb4, 0x73, 0x56, 0x88, 0xe6, 0x82, + 0xfd, 0x2f, 0x2f, 0xc0, 0x30, 0x7f, 0x72, 0x23, 0xa7, 0x94, 0xc2, 0x6e, 0xf4, 0x2, 0xf6, 0xa3, 0xe0, 0x5d, 0xc, + 0x4c, 0xf7, 0x4e, 0x60, 0x12, 0x2, 0xca, 0x40, 0xc7, 0x1a, 0x4e, 0x3c, 0x7c, 0xce, 0xd8, 0x1b, 0xba, 0x5, 0x89, + 0xf, 0xec, 0xfe, 0x4, 0x96, 0xd, 0x99, 0x2b, 0xb6, 0xbd, 0x6e, 0xa5, 0x6d, 0x28, 0xd4, 0xc0, 0x19, 0xe3, 0x91, + 0x49, 0xe1, 0xa7, 0x84, 0xc8, 0xe7, 0x77, 0x56, 0x57, 0x94, 0xa2, 0x2e, 0x49, 0xf7, 0x8b, 0x1c, 0x8f, 0x85, + 0x75, 0x8f, 0x69, 0x97, 0x6b, 0xc2, 0x4d, 0xd9, 0x88, 0x23, 0xdc, 0x50, 0xb3, 0xa, 0xbb, 0x19, 0xbc, 0xe0, + 0x21, 0x76, 0xde, 0xe9, 0x46, 0x5d, 0x32, 0x6b, 0x8d, 0x4b, 0x99, 0xd4, 0x85, 0xc, 0x77, 0x29, 0x1f, 0x6b, + 0x8f, 0xf2, 0x2, 0x89, 0xef, 0x63, 0xab, 0xc, 0xe1, 0xd6, 0x72, 0xe3, 0xdf, 0x88, 0xd0, 0x2c, 0xdf, 0xae, 0xd6, + 0xf9, 0x29, 0x7, 0x94, 0x93, 0x10, 0x66, 0x57, 0x78, 0x6b, 0x8e, 0x84, 0x11, 0x6d, 0x2b, 0xe5, 0x8a, 0xa5, + 0x65, 0x98, 0xfc, 0x92, 0x47, 0x1d, 0xfd, 0x4b, 0x41, 0xdc, 0x87, 0x42, 0xf8, 0xbc, 0x9e, 0xc1, 0xfa, 0x91, + 0x81, 0x11, 0xcc, 0x92, 0x2f, 0xdb, 0xd8, 0xf8, 0x2e, 0x56, 0x6f, 0xbe, 0xf8, 0x6c, 0xf8, 0x5f, 0x2d, 0x61, + 0xb4, 0x4c, 0x7c, 0x6c, 0x11, 0x46, 0x4a, 0x63, 0x51, 0x55, 0x5a, 0xf5, 0xfa, 0x26, 0xc7, 0x45, 0x3, 0x64, + 0x84, 0xcb, 0x69, 0x36, 0x9b, 0x5, 0x1f, 0x1c, 0x10, 0x68, 0xbc, 0x12, 0x70, 0xde, 0x83, 0xdf, 0x94, 0x9b, + 0x94, 0x6f, 0xae, 0xdb, 0xe8, 0xef, 0xd7, 0x6c, 0x23, 0xd0, 0x6f, 0x8c, 0xc5, 0x6b, 0x37, 0xad, 0x72, 0x16, + 0x69, 0x77, 0x95, 0xb4, 0x6d, 0x6e, 0x1, 0x9f, 0xf0, 0xbd, 0xd5, 0x1f, 0x5b, 0x7a, 0xc5, 0xe3, 0x2d, 0xf7, + 0x3e, 0xd1, 0xce, 0xbe, 0x89, 0x34, 0xfa, 0x3d, 0x93, 0x6c, 0x90, 0x53, 0x3d, 0xfd, 0x7, 0xc4, 0xb3, 0xb, 0x9a, + 0x22, 0xba, 0xaa, 0x50, 0x20, 0xaa, 0x4f, 0x72, 0xb6, 0xf9, 0xed, 0xc0, 0x89, 0xef, 0xab, 0x66, 0x5a, 0x47, + 0x3, 0x6f, 0xc7, 0xb4, 0x10, 0x0, 0x89, 0x7, 0x4f, 0xa, 0xbe, 0x64, 0xfb, 0x5b, 0x33, 0x28, 0xbe, 0x5a, 0xe6, + 0x9c, 0x1b, 0x94, 0x5f, 0x1, 0x88, 0x6, 0xbc, 0xbf, 0x81, 0x98, 0x97, 0x54, 0xe7, 0x52, 0xa0, 0xa, 0x37, 0xf, + 0xf0, 0x3f, 0xba, 0x7, 0xc1, 0xbc, 0xbe, 0x36, 0x31, 0x94, 0x5d, 0x7a, 0x8c, 0x2d, 0xbe, 0xc, 0x2c, 0xb2, 0x71, + 0xa2, 0x73, 0xb4, 0xfc, 0xd2, 0xc4, 0x24, 0x5, 0x64, 0xd1, 0x15, 0xf6, 0x61, 0x7c, 0x60, 0x90, 0x49, 0x8c, + 0x69, 0x68, 0xa3, 0x73, 0xf9, 0x1, 0x7, 0xfa, 0x4f, 0x2a, 0xd0, 0x52, 0x84, 0x7c, 0x5c, 0x47, 0x78, 0x3c, 0xac, + 0xc4, 0x5c, 0x18, 0x32, 0x6c, 0x5e, 0xbb, 0xdb, 0xbd, 0x3c, 0x84, 0xc9, 0x9f, 0x65, 0xab, 0x3f, 0xa, 0x53, + 0x2c, 0xa1, 0x50, 0x4a, 0x45, 0x62, 0x46, 0xaa, 0xb4, 0xe8, 0x49, 0x11, 0x33, 0xa1, 0x32, 0x30, 0x2d, 0xf0, + 0xb2, 0x2f, 0x60, 0x68, 0x4a, 0x3b, 0x27, 0xd3, 0xb, 0xfa, 0x53, 0xc8, 0xdb, 0x99, 0xea, 0x8c, 0x67, 0x57, + 0x25, 0x26, 0xe, 0xc5, 0x98, 0xd0, 0xaf, 0xff, 0xac, 0x83, 0x85, 0x8f, 0x21, 0x4b, 0x91, 0xdd, 0x79, 0x40, + 0x9e, 0x36, 0x1f, 0x28, 0x62, 0xc9, 0xd3, 0x32, 0xe8, 0xc0, 0x62, 0x1b, 0x5b, 0xd5, 0x16, 0xf9, 0x2f, 0xa5, + 0x46, 0x73, 0xb3, 0x0, 0xc9, 0xbf, 0x3c, 0x31, 0x60, 0x3a, 0xbf, 0xc3, 0xdd, 0xc9, 0xb0, 0x8b, 0x7c, 0x2, 0x39, + 0x88, 0x37, 0x15, 0xfb, 0x81, 0x59, 0x80, 0xb2, 0xd9, 0x62, 0xf, 0xac, 0x85, 0x82, 0x8a, 0x49, 0x4, 0xea, 0x4a, + 0x1d, 0xb2, 0x29, 0x81, 0xba, 0xd7, 0xe4, 0x1a, 0x7f, 0x5d, 0x95, 0xf8, 0xcc, 0x70, 0x33, 0x79, 0x91, 0x85, + 0xf0, 0xc, 0xbb, 0x62, 0x3a, 0xcb, 0x1a, 0x6, 0x59, 0x88, 0x73, 0xb7, 0x54, 0xf6, 0x2a, 0xdb, 0xb4, 0xd3, 0x6a, + 0xe2, 0x72, 0x33, 0xf5, 0x55, 0xa9, 0x9c, 0xb8, 0xb6, 0xf2, 0x69, 0x65, 0xde, 0xc5, 0xf9, 0x9a, 0x25, 0xe6, + 0x9b, 0x38, 0x74, 0xe0, 0x17, 0x47, 0x96, 0x84, 0xce, 0x6c, 0x35, 0xb7, 0xfd, 0x90, 0x8e, 0xaf, 0x48, 0x41, + 0x17, 0xd8, 0xd8, 0xe0, 0x45, 0x1, 0x1b, 0xea, 0x75, 0x5b, 0x4, 0xfb, 0xf2, 0x8d, 0x47, 0xfd, 0x4f, 0xf1, 0xd0, + 0xd1, 0x33, 0xab, 0xce, 0x57, 0xd1, 0x3f, 0xde, 0xc, 0xf1, 0xbe, 0x2b, 0x81, 0xe2, 0x45, 0x4a, 0x2f, 0x25, + 0x16, 0xf7, 0x9f, 0x8, 0x98, 0x7f, 0x4a, 0x31, 0xd7, 0x66, 0xf9, 0xa0, 0x5c, 0x4e, 0xee, 0x4f, 0x3f, 0xbc, + 0x4a, 0x4a, 0xcd, 0x2c, 0x28, 0x25, 0xbc, 0xe1, 0x47, 0x8d, 0xd2, 0x92, 0x5, 0xa, 0x67, 0xb2, 0x49, 0x4f, 0x47, + 0xe8, 0xe5, 0x63, 0x92, 0xe0, 0xd2, 0x35, 0xed, 0xd9, 0xb6, 0x7b, 0x37, 0x7f, 0x20, 0xc4, 0xe4, 0xc4, 0x20, + 0xbb, 0xd, 0x88, 0x29, 0xbe, 0x68, 0x87, 0xf, 0xfe, 0xb, 0xff, 0x2, 0x6b, 0xca, 0x47, 0x40, 0x80, 0x15, 0xc7, + 0xcc, 0x7f, 0x31, 0x20, 0x62, 0x59, 0x51, 0xe5, 0x35, 0x57, 0x27, 0x6b, 0x34, 0x3e, 0x1c, 0xa1, 0x32, 0x3b, + 0x8b, 0xeb, 0x6a, 0xa6, 0x4a, 0xe8, 0x7d, 0xaa, 0x98, 0x4c, 0x6e, 0x97, 0x1c, 0x5b, 0x15, 0xef, 0x57, 0xd7, + 0x4f, 0x8a, 0x5f, 0xde, 0x49, 0xc, 0x80, 0xa5, 0xcb, 0x19, 0x85, 0xe5, 0xbe, 0x16, 0x32, 0x6b, 0xd1, 0xf2, + 0xe0, 0xff, 0x58, 0x31, 0xe5, 0x55, 0x72, 0xbf, 0xa5, 0x2e, 0x7f, 0x9, 0x2c, 0x77, 0xc4, 0xec, 0x75, 0xcd, + 0x11, 0x31, 0x78, 0x39, 0xed, 0x79, 0x7d, 0x3, 0x70, 0xac, 0xa3, 0x2a, 0x8f, 0xe7, 0x7b, 0x2, 0xa0, 0x1b, 0x1c, + 0xf9, 0x36, 0x36, 0x91, 0x31, 0x71, 0x80, 0x3a, 0x2e, 0x6d, 0x2d, 0x73, 0xf1, 0x8c, 0xfe, 0x6, 0x2c, 0x1c, + 0x69, 0x8f, 0xae, 0x46, 0x5a, 0xdc, 0xfa, 0x3b, 0x3d, 0x2a, 0x50, 0xd1, 0x3f, 0xd0, 0xa5, 0xfb, 0x21, 0xfe, + 0xc8, 0x60, 0x6f, 0x71, 0xb8, 0xa9, 0xbf, 0xe5, 0x35, 0x80, 0x3b, 0xde, 0xb4, 0xdf, 0xca, 0xa0, 0x1e, 0xbc, + 0x9b, 0xcb, 0xc2, 0x9e, 0x83, 0x44, 0xf9, 0x9f, 0x92, 0x57, 0x22, 0x2f, 0x19, 0xa3, 0x73, 0x85, 0x62, 0x86, + 0x2d, 0xce, 0xf6, 0x3d, 0xb3, 0x8e, 0xeb, 0xaa, 0x9e, 0xe6, 0x1f, 0xf5, 0x51, 0x3e, 0xf3, 0xb8, 0xd9, 0x44, + 0x55, 0x1e, 0xbe, 0x89, 0x2f, 0x6a, 0xb5, 0xfb, 0x83, 0xc7, 0x55, 0x5c, 0xaa, 0xa0, 0x1c, 0x15, 0x3e, 0xd4, + 0x2e, 0x2e, 0x36, 0x69, 0x2f, 0x8e, 0xcb, 0xc7, 0xea, 0x3d, 0x79, 0x2e, 0xf0, 0x2e, 0xa6, 0xd8, 0x39, 0x3b, + 0xa4, 0x99, 0xae, 0xc, 0x8f, 0x3e, 0x77, 0x2a, 0xf3, 0x93, 0x8d, 0xdc, 0x79, 0xbe, 0x79, 0x1c, 0xe6, 0xf9, + 0xc4, 0x78, 0x38, 0xce, 0x93, 0x96, 0xf7, 0x3a, 0xa5, 0x54, 0x70, 0xe6, 0x6f, 0x8a, 0x61, 0x3e, 0xdf, 0xd9, + 0x72, 0x2f, 0x2e, 0x3d, 0xeb, 0xb6, 0x29, 0xc6, 0x87, 0xb2, 0xf9, 0x72, 0x2, 0x56, 0x67, 0x74, 0xec, 0xe7, + 0xa2, 0xd2, 0xc2, 0x9f, 0xa, 0x9e, 0x22, 0xd2, 0xf5, 0xf7, 0x74, 0x24, 0x4f, 0xfc, 0x8f, 0xdf, 0xab, 0xe0, + 0x34, 0x80, 0xa0, 0x82, 0xab, 0x44, 0x19, 0x28, 0x75, 0x74, 0xe5, 0x1b, 0x75, 0xff, 0xfc, 0x94, 0x4d, 0x80, + 0xea, 0x94, 0x85, 0xd6, 0xf5, 0x8b, 0x39, 0x51, 0x7c, 0x12, 0x3f, 0x6a, 0xf0, 0xa1, 0xed, 0xfd, 0x52, 0x2d, + 0x90, 0xa8, 0x4b, 0x2b, 0xe1, 0x3b, 0x37, 0xc4, 0xe8, 0x98, 0xfb, 0x1c, 0x5f, 0xdc, 0x5d, 0x2f, 0xde, 0xae, + 0xd0, 0xa, 0x5c, 0x3a, 0x1f, 0xce, 0x37, 0x7f, 0x9d, 0x84, 0xb9, 0xc7, 0xe2, 0xfc, 0xf7, 0x73, 0xee, 0x79, + 0xbf, 0x6a, 0x62, 0x6a, 0x8c, 0x0, 0x54, 0x3e, 0xca, 0x66, 0x7e, 0xf0, 0xf9, 0x5c, 0xd3, 0xc9, 0xd, 0xd8, 0x85, + 0x3e, 0xa7, 0x19, 0x91, 0xf5, 0x90, 0x51, 0xbb, 0xfb, 0x63, 0xec, 0x25, 0x12, 0x14, 0x3, 0xce, 0xde, 0x7, 0xf7, + 0x59, 0x8f, 0x73, 0xb4, 0xe2, 0xf1, 0x32, 0x35, 0x21, 0x2f, 0x0, 0x61, 0x69, 0x5e, 0x2f, 0x12, 0x4f, 0x85, + 0xbc, 0xb3, 0x14, 0x85, 0x1f, 0x54, 0x6d, 0xa7, 0xfe, 0x47, 0xe8, 0xc4, 0xb2, 0x3f, 0xf4, 0x15, 0xf8, 0xc6, + 0xdd, 0x24, 0x14, 0x81, 0xa8, 0xe, 0xb1, 0x9b, 0x59, 0x60, 0x86, 0x64, 0x92, 0xc2, 0x5c, 0xc2, 0x2d, 0x92, + 0xcc, 0x9e, 0xbb, 0xa8, 0xed, 0xe5, 0x2b, 0xd1, 0x40, 0xdf, 0xdd, 0x57, 0x8d, 0x96, 0x3, 0x34, 0x6e, 0xbf, + 0xaf, 0xfd, 0x0, 0xfd, 0x4f, 0x0, 0x4, 0x35, 0xb8, 0x2b, 0x3b, 0xec, 0x33, 0x48, 0x94, 0xd0, 0x3c, 0x16, 0x98, + 0x9, 0xd1, 0xbc, 0x0, 0x36, 0xb3, 0x9d, 0x60, 0x7f, 0xab, 0x29, 0xe4, 0xca, 0xc0, 0x30, 0xd7, 0x45, 0x7d, 0xd7, + 0xb7, 0xf2, 0xc5, 0x6e, 0x7c, 0x9a, 0x5d, 0xe8, 0xfc, 0x2c, 0x27, 0x7b, 0xdb, 0x30, 0xb9, 0xc3, 0x51, 0x6c, + 0xc7, 0x35, 0x0, 0xeb, 0x8c, 0x27, 0xf9, 0x70, 0xe4, 0x54, 0xee, 0xb0, 0xe6, 0xbe, 0xb8, 0x94, 0x14, 0x6f, + 0xf5, 0xa6, 0x28, 0xa2, 0xc3, 0x7e, 0x3c, 0x93, 0x5a, 0xdd, 0x79, 0x44, 0x7f, 0x9c, 0xf8, 0xde, 0x6c, 0xfa, + 0xda, 0xa9, 0xe5, 0x42, 0x99, 0x4e, 0x60, 0x21, 0x9e, 0x12, 0x4a, 0x94, 0xb5, 0x9b, 0xa6, 0x0, 0x57, 0x23, + 0x7b, 0xcf, 0x38, 0xe8, 0xe3, 0xcb, 0x1d, 0xf8, 0x13, 0x9f, 0x2f, 0x35, 0x91, 0xb4, 0xd8, 0xac, 0xe8, 0xe7, + 0xb2, 0x87, 0x3f, 0xac, 0xd, 0xcc, 0x6f, 0xbd, 0xfb, 0x5f, 0xd6, 0x68, 0x37, 0x61, 0xe8, 0x71, 0xbe, 0xc6, + 0xc0, 0xae, 0x52, 0xf5, 0x1d, 0xc1, 0x1, 0xc8, 0xfa, 0x89, 0xf, 0x3b, 0x90, 0x57, 0xa5, 0x99, 0xec, 0xa, 0xb8, + 0xab, 0x6d, 0x12, 0x9a, 0x8b, 0xe8, 0xcc, 0xe0, 0x30, 0x9f, 0x42, 0x35, 0x7, 0xa0, 0x4a, 0x71, 0xab, 0x6d, + 0x82, 0x98, 0x9b, 0x7f, 0xb3, 0xc7, 0x6b, 0x25, 0x6f, 0xe5, 0x86, 0xa8, 0xdb, 0xb2, 0x7f, 0x37, 0xb1, 0xd3, + 0x46, 0xe9, 0xe6, 0x92, 0xbe, 0x69, 0x90, 0xbf, 0xc5, 0xf0, 0xb4, 0xe1, 0x22, 0xe4, 0xc5, 0x7d, 0x1a, 0x28, + 0x50, 0x5b, 0x36, 0x3d, 0x86, 0x80, 0x92, 0x22, 0xed, 0xc6, 0x5d, 0x2a, 0x3f, 0xad, 0x75, 0xb1, 0x3a, 0x5e, + 0xae, 0xed, 0x4f, 0xa5, 0xaa, 0x48, 0x85, 0x5f, 0xb8, 0x21, 0xa, 0x55, 0x5d, 0x2f, 0x2c, 0x17, 0xf7, 0xac, + 0x38, 0xa3, 0x95, 0x62, 0x86, 0xc2, 0xa3, 0x6a, 0x44, 0xdc, 0xb3, 0x43, 0x44, 0xc3, 0x3b, 0xc8, 0xc2, 0x5d, + 0xe8, 0x12, 0x35, 0xae, 0xfd, 0x26, 0x75, 0x87, 0x6, 0x7, 0xe0, 0x81, 0xe7, 0x4b, 0x2f, 0x54, 0xd6, 0x4a, 0x1c, + 0xb9, 0x2f, 0xfb, 0xad, 0x6d, 0x32, 0x94, 0x68, 0x65, 0x6, 0xa9, 0x9f, 0xf4, 0xe9, 0x8e, 0xaf, 0x6, 0xb0, 0x41, + 0x11, 0xbb, 0x98, 0xec, 0x76, 0x40, 0x7a, 0xd, 0xa7, 0x7, 0x77, 0x1b, 0xa0, 0x7, 0x8b, 0x84, 0x9e, 0xeb, 0xda, + 0x80, 0xcf, 0xc, 0x48, 0xa9, 0x7f, 0x27, 0xcd, 0xa5, 0x1a, 0x92, 0xfc, 0xde, 0x99, 0xe2, 0x97, 0x83, 0x80, + 0x65, 0xbf, 0xb1, 0xba, 0xed, 0xdb, 0x3a, 0xe0, 0xee, 0xc2, 0x2, 0x8, 0x21, 0x60, 0xfe, 0x91, 0x7a, 0xcd, 0x92, + 0xd3, 0x12, 0x13, 0x19, 0x82, 0x90, 0xd2, 0xb4, 0x9a, 0xd7, 0x32, 0xd0, 0xf2, 0x85, 0x99, 0xa4, 0x0, 0xcb, + 0x4f, 0x8a, 0xf8, 0x86, 0x50, 0x11, 0x23, 0x53, 0xe9, 0x6a, 0xe, 0x2a, 0x19, 0x7c, 0xe8, 0x80, 0x17, 0x9e, + 0xc9, 0xf4, 0x19, 0x28, 0xb6, 0x32, 0x24, 0x61, 0xac, 0xe5, 0xaa, 0x11, 0x53, 0x1b, 0x34, 0x1f, 0x24, 0x85, + 0x14, 0x86, 0xb1, 0x9e, 0x2e, 0x4d, 0x82, 0x12, 0x69, 0x6a, 0xbb, 0xb3, 0x8f, 0xf3, 0xc2, 0x40, 0x7c, 0xbd, + 0xad, 0x31, 0xdd, 0x6, 0xf5, 0x16, 0x3c, 0xb, 0x43, 0x31, 0x1, 0x39, 0xfd, 0xa, 0xfe, 0x5d, 0x5d, 0x59, 0x1a, + 0xf1, 0x3a, 0xc5, 0xc5, 0xeb, 0x30, 0x57, 0xa4, 0xdc, 0xf4, 0xfa, 0xa3, 0xf, 0x71, 0xb6, 0x52, 0xb8, 0x73, 0x5, + 0x8a, 0xcf, 0xdb, 0x9b, 0xdb, 0x65, 0xdb, 0x64, 0xbd, 0x27, 0x45, 0xde, 0xe6, 0x50, 0x0, 0xa, 0x1e, 0x4d, 0xce, + 0x98, 0xe0, 0x2e, 0x2e, 0xe0, 0xd9, 0x44, 0xbf, 0x86, 0x60, 0xb1, 0x33, 0xb3, 0x33, 0xaa, 0x8e, 0x5, 0x96, + 0x10, 0xca, 0x4b, 0x2c, 0x56, 0xdf, 0x42, 0x59, 0xdc, 0x93, 0xc1, 0x79, 0xed, 0x0, 0x54, 0x44, 0x40, 0xf7, + 0x86, 0xaa, 0x8e, 0xbb, 0x5c, 0x1a, 0x6, 0x28, 0xea, 0x11, 0xab, 0xba, 0x6a, 0x36, 0x1c, 0x30, 0x6c, 0x66, + 0x11, 0xce, 0xf4, 0xd, 0xab, 0xed, 0xa0, 0x50, 0xeb, 0x13, 0xe1, 0x22, 0x1a, 0x75, 0x68, 0x29, 0x62, 0x10, + 0xae, 0x68, 0x80, 0xf6, 0xa7, 0xd4, 0x9d, 0xcd, 0x2c, 0x22, 0x6c, 0xab, 0x44, 0x4d, 0x53, 0xad, 0x1c, 0x2, + 0x5d, 0x3, 0xbb, 0x9b, 0x41, 0x97, 0x43, 0x46, 0x24, 0x34, 0xdf, 0x7a, 0xaf, 0x27, 0xbd, 0xac, 0x4f, 0x31, + 0xed, 0xeb, 0x6d, 0x9f, 0x46, 0x2d, 0x78, 0xc7, 0xad, 0x60, 0xea, 0x21, 0x52, 0x1f, 0x66, 0x4, 0xd4, 0xe2, 0x0, + 0x3, 0xda, 0xee, 0x4a, 0xa, 0xc7, 0x5b, 0x70, 0x12, 0x9f, 0xb7, 0xa2, 0x6a, 0x7d, 0xca, 0x28, 0x3a, 0xc6, 0x5e, + 0x57, 0x2, 0xb1, 0x88, 0x29, 0x72, 0xa4, 0x82, 0x9a, 0x59, 0xb6, 0xc0, 0x63, 0xbb, 0x88, 0xc5, 0x15, 0x3f, + 0x33, 0xf5, 0x24, 0x54, 0xbc, 0x16, 0x7c, 0xc6, 0xda, 0xd8, 0xfa, 0xb9, 0xe5, 0xca, 0x1e, 0x49, 0xdc, 0x2a, + 0x25, 0x42, 0xe, 0x58, 0xa1, 0x60, 0x1, 0x91, 0x1d, 0x8a, 0xaf, 0x91, 0x60, 0x9, 0x17, 0xed, 0xc8, 0x28, 0xcf, + 0x88, 0xb0, 0xb0, 0x40, 0xca, 0x6d, 0xe1, 0x9, 0xe, 0x19, 0x4a, 0xc2, 0x48, 0xba, 0x5a, 0x22, 0xfe, 0x4b, 0xe2, + 0x38, 0xe6, 0xe0, 0x44, 0x33, 0x8b, 0xcd, 0xc, 0x25, 0x2b, 0x89, 0x68, 0x64, 0x61, 0x55, 0x3a, 0x55, 0xb2, + 0x7c, 0xbc, 0xe6, 0x93, 0x62, 0x5c, 0xc1, 0x76, 0x1c, 0x5c, 0x1e, 0x59, 0x3f, 0xcb, 0x39, 0x53, 0x19, 0xf3, + 0x7e, 0xaf, 0xc3, 0xab, 0x74, 0x78, 0x73, 0x73, 0x4d, 0x2f, 0xe6, 0x9f, 0x3c, 0xb1, 0xc8, 0x77, 0xbe, 0x16, + 0xd2, 0xe5, 0x8, 0xe1, 0x71, 0xce, 0xb4, 0x2, 0xd7, 0x69, 0x1, 0x3e, 0x49, 0x31, 0x27, 0x5, 0xad, 0x92, 0xdf, + 0x10, 0x90, 0xc5, 0x54, 0x76, 0xc4, 0x4f, 0x8f, 0x14, 0x8d, 0xff, 0x88, 0xd0, 0x50, 0x20, 0x8d, 0x70, 0x8a, + 0xcb, 0x43, 0xab, 0x31, 0x30, 0xec, 0x7b, 0x93, 0x71, 0xfc, 0xc3, 0x68, 0x4a, 0x72, 0x3e, 0xe7, 0x84, 0xce, + 0x91, 0x68, 0xf, 0xfa, 0xb9, 0xce, 0x4f, 0x7a, 0xde, 0x18, 0x12, 0x90, 0xf5, 0xf2, 0xce, 0xc5, 0xc5, 0xd6, + 0x64, 0x5b, 0xa1, 0x66, 0xe0, 0x3a, 0xff, 0xd, 0x49, 0xb1, 0x5d, 0x2c, 0x6e, 0x3e, 0x5b, 0x86, 0x2e, 0xd7, + 0xae, 0xd5, 0xc1, 0xcc, 0xc7, 0x3b, 0x44, 0x1f, 0x72, 0x67, 0x84, 0x6, 0xbb, 0x5c, 0x13, 0xff, 0x20, 0xd8, + 0x21, 0x22, 0xd6, 0x9c, 0x2b, 0xa6, 0x47, 0x85, 0xbd, 0xcf, 0xc6, 0xc3, 0x4d, 0x5d, 0xee, 0x7d, 0x63, 0x4f, + 0xe4, 0xab, 0x37, 0xfc, 0x95, 0x84, 0xbd, 0xc8, 0x14, 0x24, 0x30, 0x43, 0x29, 0xc6, 0x8d, 0xd5, 0xa7, 0x84, + 0xf6, 0x4a, 0x74, 0x54, 0x7e, 0x4b, 0x4d, 0xba, 0xbc, 0x2c, 0xa8, 0xe0, 0x3b, 0xbd, 0xd4, 0x8d, 0x32, 0xf8, + 0x92, 0x91, 0x3f, 0x42, 0xdb, 0xbe, 0xac, 0x66, 0x8d, 0x78, 0x3f, 0xca, 0xe9, 0x45, 0x78, 0xd7, 0x2f, 0xec, + 0xfa, 0xb6, 0x7c, 0x92, 0xed, 0xb, 0xff, 0xa0, 0xb7, 0xd4, 0xfb, 0xe8, 0x6e, 0x63, 0x52, 0x54, 0xa9, 0x64, + 0x8e, 0x61, 0xd6, 0x1a, 0x7f, 0x97, 0x53, 0x41, 0xb2, 0xa0, 0xf2, 0x4c, 0xff, 0x54, 0xd7, 0x9c, 0x90, 0xcf, + 0x3, 0xa3, 0xc6, 0x59, 0xb2, 0xb2, 0xe0, 0xb2, 0xe2, 0xd6, 0x54, 0xd8, 0xad, 0x71, 0xf4, 0xae, 0xc7, 0x7a, + 0xfd, 0x30, 0xb0, 0x6c, 0xa2, 0x4b, 0xc4, 0x2e, 0x59, 0xab, 0xcd, 0xb4, 0xf4, 0xe7, 0xde, 0xad, 0x14, 0x1b, + 0x64, 0x42, 0x4b, 0x5d, 0xf3, 0xba, 0xf3, 0x7a, 0xb9, 0xb7, 0xd1, 0xcb, 0x42, 0xec, 0x23, 0x8a, 0xbb, 0x44, + 0x79, 0x58, 0x9e, 0x79, 0x38, 0x20, 0x8, 0x0, 0x8e, 0xa7, 0xbf, 0xbd, 0x75, 0x69, 0xd5, 0x4a, 0x83, 0x62, 0x70, + 0x9b, 0xb3, 0xf2, 0x28, 0xdb, 0xf, 0x8d, 0xad, 0xf3, 0xb9, 0xa9, 0x2d, 0x16, 0x42, 0x1b, 0x55, 0x1b, 0x66, + 0xfa, 0x14, 0xa8, 0x29, 0x49, 0x23, 0xb2, 0xfd, 0x68, 0x9, 0x69, 0x7d, 0x74, 0xc6, 0x4f, 0xc8, 0x29, 0xc5, + 0x8a, 0x9a, 0xde, 0x7, 0xd3, 0x78, 0xf4, 0xe6, 0x5b, 0x8e, 0x34, 0xc4, 0xf, 0x9, 0x0, 0x68, 0xce, 0x2d, 0x81, + 0x27, 0x24, 0x2f, 0xcd, 0x70, 0x5c, 0x86, 0x9c, 0x2d, 0x10, 0xee, 0xd4, 0x9a, 0x1a, 0xdf, 0x2c, 0xc8, 0x65, + 0x8a, 0xd4, 0xd7, 0x6d, 0x10, 0x16, 0x9f, 0xeb, 0xc7, 0x64, 0x61, 0xb4, 0xbd, 0x51, 0x7b, 0x88, 0xc5, 0x50, + 0x95, 0xc7, 0xa4, 0x7e, 0xa9, 0x74, 0x44, 0x9e, 0xd5, 0x56, 0x95, 0x4, 0xd8, 0xb0, 0xb2, 0x5c, 0x4f, 0x5, 0x82, + 0x2b, 0xfc, 0x1e, 0x53, 0x9d, 0xe2, 0x10, 0xaa, 0x32, 0x51, 0x91, 0x1d, 0xeb, 0x47, 0x4, 0x6, 0x70, 0xd6, 0x61, + 0xb6, 0xc8, 0xe3, 0xd, 0x52, 0x84, 0xdb, 0x9c, 0x55, 0x6f, 0x46, 0x6c, 0x50, 0xb6, 0x62, 0x42, 0x31, 0xfa, + 0x63, 0x9d, 0x97, 0xa7, 0x9c, 0x31, 0xb1, 0x43, 0x54, 0x4f, 0x5f, 0x7e, 0x6a, 0x34, 0x91, 0x38, 0x68, 0x9b, + 0x6b, 0x5e, 0xc1, 0x48, 0x5, 0x9d, 0x1b, 0x7e, 0x95, 0x63, 0x64, 0x9f, 0x3c, 0x4c, 0x71, 0x5c, 0x9b, 0x87, + 0xce, 0x31, 0x5c, 0xc7, 0xa7, 0x24, 0x16, 0xdb, 0x83, 0xdb, 0x54, 0xea, 0x6a, 0xb8, 0xe, 0xd6, 0xba, 0x10, + 0xeb, 0xe3, 0x7c, 0xa0, 0xcc, 0x91, 0xe8, 0x6, 0x69, 0x66, 0x60, 0xef, 0x55, 0x4a, 0x3e, 0xf9, 0xe6, 0xb4, + 0x5b, 0x48, 0xb6, 0xf4, 0xeb, 0x8, 0xe6, 0xc3, 0xb5, 0x22, 0xf8, 0x18, 0x1b, 0xc0, 0xb6, 0xdb, 0x87, 0x8b, + 0x13, 0x3c, 0x21, + ]; + + let mac_data = vec![ + 0xe8, 0x38, 0x46, 0xb7, 0xa8, 0xf9, 0x5c, 0x66, 0xe3, 0x8e, 0x10, 0x8f, 0xb6, 0x4, 0x80, 0xc7, + ]; + + let upgrade_license = ServerUpgradeLicense { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::NewLicense, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: u16::try_from( + PREAMBLE_SIZE + BLOB_LENGTH_SIZE + BLOB_TYPE_SIZE + encrypted_license_info.len() + MAC_SIZE, + ) + .expect("can't panic"), + }, + encrypted_license_info, + mac_data, + }; + + let encryption_info = LicenseEncryptionData { + premaster_secret: Vec::new(), // this field is not involved in this unit test + mac_salt_key: vec![ + 0xd5, 0x2c, 0x7c, 0xd2, 0x71, 0x15, 0x2c, 0x41, 0xbb, 0xd8, 0x36, 0xdb, 0x19, 0x3e, 0xc0, 0xf3, + ], + license_key: vec![ + 0x88, 0x7d, 0x33, 0xa6, 0x13, 0xd, 0x76, 0xbf, 0x76, 0x2a, 0xf, 0x57, 0x71, 0x1d, 0x40, 0xa3, + ], + }; + + upgrade_license.verify_server_license(&encryption_info).unwrap(); +} diff --git a/crates/ironrdp-pdu/src/rdp/server_license/tests.rs b/crates/ironrdp-pdu/src/rdp/server_license/tests.rs new file mode 100644 index 00000000..84bbb408 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/server_license/tests.rs @@ -0,0 +1,159 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const LICENSE_HEADER_BUFFER: [u8; 8] = [ + 0x80, 0x00, // flags + 0x00, 0x00, // flagsHi + 0xff, 0x03, 0x10, 0x00, +]; + +const BLOB_BUFFER: [u8; 76] = [ + 0x08, 0x00, // sig blob type + 0x48, 0x00, // sig blob len + 0xe9, 0xe1, 0xd6, 0x28, 0x46, 0x8b, 0x4e, 0xf5, 0x0a, 0xdf, 0xfd, 0xee, 0x21, 0x99, 0xac, 0xb4, 0xe1, 0x8f, 0x5f, + 0x81, 0x57, 0x82, 0xef, 0x9d, 0x96, 0x52, 0x63, 0x27, 0x18, 0x29, 0xdb, 0xb3, 0x4a, 0xfd, 0x9a, 0xda, 0x42, 0xad, + 0xb5, 0x69, 0x21, 0x89, 0x0e, 0x1d, 0xc0, 0x4c, 0x1a, 0xa8, 0xaa, 0x71, 0x3e, 0x0f, 0x54, 0xb9, 0x9a, 0xe4, 0x99, + 0x68, 0x3f, 0x6c, 0xd6, 0x76, 0x84, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // blob data +]; + +const PLATFORM_CHALLENGE_BUFFER: [u8; 42] = [ + 0x80, 0x00, // flags + 0x00, 0x00, // flagsHi + 0x02, 0x03, 0x26, 0x00, // preamble + 0x00, 0x00, 0x00, 0x00, // connect flags + 0x00, 0x00, // ignored + 0x0a, 0x00, // blob len + 0x46, 0x37, 0x85, 0x54, 0x8e, 0xc5, 0x91, 0x34, 0x97, 0x5d, // challenge + 0x38, 0x23, 0x62, 0x5d, 0x10, 0x8b, 0x93, 0xc3, 0xf1, 0xe4, 0x67, 0x1f, 0x4a, 0xb6, 0x00, 0x0a, // mac data +]; + +const STATUS_VALID_CLIENT_BUFFER: [u8; 20] = [ + 0x80, 0x00, // flags + 0x00, 0x00, // flagsHi + 0xff, 0x03, 0x10, 0x00, 0x07, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, +]; + +static LICENSE_HEADER: LazyLock = LazyLock::new(|| LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::ErrorAlert, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: 0x10, +}); + +#[test] +fn read_blob_header_handles_wrong_type_correctly() { + let h = decode::(&BLOB_BUFFER).unwrap(); + assert_ne!(h.blob_type, BlobType::CERTIFICATE); +} + +#[test] +fn read_blob_header_handles_invalid_type_correctly() { + let invalid_blob_buffer: [u8; 76] = [ + 0x99, 0x00, // sig blob type + 0x48, 0x00, // sig blob len + 0xe9, 0xe1, 0xd6, 0x28, 0x46, 0x8b, 0x4e, 0xf5, 0x0a, 0xdf, 0xfd, 0xee, 0x21, 0x99, 0xac, 0xb4, 0xe1, 0x8f, + 0x5f, 0x81, 0x57, 0x82, 0xef, 0x9d, 0x96, 0x52, 0x63, 0x27, 0x18, 0x29, 0xdb, 0xb3, 0x4a, 0xfd, 0x9a, 0xda, + 0x42, 0xad, 0xb5, 0x69, 0x21, 0x89, 0x0e, 0x1d, 0xc0, 0x4c, 0x1a, 0xa8, 0xaa, 0x71, 0x3e, 0x0f, 0x54, 0xb9, + 0x9a, 0xe4, 0x99, 0x68, 0x3f, 0x6c, 0xd6, 0x76, 0x84, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // blob data + ]; + + let header = decode::(&invalid_blob_buffer).unwrap(); + assert_eq!( + header, + BlobHeader { + blob_type: BlobType(0x99), + length: 0x48 + } + ) +} + +#[test] +fn read_blob_header_reads_blob_correctly() { + let blob = decode::(&BLOB_BUFFER).unwrap(); + assert_eq!(blob.blob_type, BlobType::RSA_SIGNATURE); + assert_eq!(blob.length, BLOB_BUFFER.len() - 4); +} + +#[test] +fn write_blob_header_writes_blob_header_correctly() { + let correct_blob_header = &BLOB_BUFFER[..4]; + let blob_data = &BLOB_BUFFER[4..]; + + let blob = BlobHeader::new(BlobType::RSA_SIGNATURE, blob_data.len()); + let buffer = encode_vec(&blob).unwrap(); + + assert_eq!(correct_blob_header, buffer.as_slice()); +} + +#[test] +fn mac_data_computes_correctly() { + let mac_salt_key: [u8; 16] = [ + 0x68, 0x1f, 0x7b, 0x26, 0x7e, 0x76, 0xa, 0x24, 0x2d, 0x98, 0x7, 0xd6, 0x6b, 0x56, 0xc5, 0x1, + ]; + + let server_mac_data: [u8; 16] = [ + 0x58, 0xaf, 0x1f, 0x30, 0xd6, 0x4e, 0xe8, 0x6, 0xfc, 0xf9, 0xe6, 0x68, 0x21, 0x64, 0x25, 0x3d, + ]; + + let decrypted_server_challenge: [u8; 10] = [0x54, 0x0, 0x45, 0x0, 0x53, 0x0, 0x54, 0x0, 0x0, 0x0]; + + assert_eq!( + compute_mac_data(mac_salt_key.as_ref(), decrypted_server_challenge.as_ref()).unwrap(), + server_mac_data.as_ref() + ); +} + +#[test] +fn from_buffer_correctly_parses_license_header() { + assert_eq!( + decode::(&LICENSE_HEADER_BUFFER).unwrap(), + *LICENSE_HEADER + ); +} + +#[test] +fn to_buffer_correctly_serializes_license_header() { + let buffer = encode_vec(&*LICENSE_HEADER).unwrap(); + + assert_eq!(buffer, LICENSE_HEADER_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_license_header() { + assert_eq!(LICENSE_HEADER.size(), PREAMBLE_SIZE + BASIC_SECURITY_HEADER_SIZE); +} + +#[test] +fn read_license_header_reads_correctly() { + decode::(&PLATFORM_CHALLENGE_BUFFER).unwrap(); +} + +#[test] +fn read_license_header_handles_valid_client_correctly() { + let pdu = decode::(&STATUS_VALID_CLIENT_BUFFER).unwrap(); + assert_eq!( + pdu, + LicensingErrorMessage { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::ErrorAlert, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: 0x10, + }, + error_code: LicenseErrorCode::StatusValidClient, + state_transition: LicensingStateTransition::NoTransition, + error_info: Vec::new() + } + .into() + ); +} diff --git a/crates/ironrdp-pdu/src/rdp/session_info/logon_extended.rs b/crates/ironrdp-pdu/src/rdp/session_info/logon_extended.rs new file mode 100644 index 00000000..88a42e5c --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/session_info/logon_extended.rs @@ -0,0 +1,271 @@ +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, Decode, DecodeResult, Encode, + EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +const LOGON_EX_LENGTH_FIELD_SIZE: usize = 2; +const LOGON_EX_FLAGS_FIELD_SIZE: usize = 4; +const LOGON_EX_PADDING_SIZE: usize = 570; +const LOGON_EX_PADDING_BUFFER: [u8; LOGON_EX_PADDING_SIZE] = [0; LOGON_EX_PADDING_SIZE]; + +const LOGON_INFO_FIELD_DATA_SIZE: usize = 4; +const AUTO_RECONNECT_VERSION_1: u32 = 0x0000_0001; +const AUTO_RECONNECT_PACKET_SIZE: usize = 28; +const AUTO_RECONNECT_RANDOM_BITS_SIZE: usize = 16; +const LOGON_ERRORS_INFO_SIZE: usize = 8; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogonInfoExtended { + pub present_fields_flags: LogonExFlags, + pub auto_reconnect: Option, + pub errors_info: Option, +} + +impl LogonInfoExtended { + const NAME: &'static str = "LogonInfoExtended"; + + const FIXED_PART_SIZE: usize = LOGON_EX_LENGTH_FIELD_SIZE + LOGON_EX_FLAGS_FIELD_SIZE; + + fn get_internal_size(&self) -> usize { + let reconnect_size = self.auto_reconnect.as_ref().map(|r| r.size()).unwrap_or(0); + + let errors_size = self.errors_info.as_ref().map(|r| r.size()).unwrap_or(0); + + Self::FIXED_PART_SIZE + reconnect_size + errors_size + } +} + +impl Encode for LogonInfoExtended { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(cast_length!("internalSize", self.get_internal_size())?); + dst.write_u32(self.present_fields_flags.bits()); + + if let Some(ref reconnect) = self.auto_reconnect { + reconnect.encode(dst)?; + } + if let Some(ref errors) = self.errors_info { + errors.encode(dst)?; + } + + dst.write_slice(LOGON_EX_PADDING_BUFFER.as_ref()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.get_internal_size() + LOGON_EX_PADDING_SIZE + } +} + +impl<'de> Decode<'de> for LogonInfoExtended { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _self_length = src.read_u16(); + let present_fields_flags = LogonExFlags::from_bits_truncate(src.read_u32()); + + let auto_reconnect = if present_fields_flags.contains(LogonExFlags::AUTO_RECONNECT_COOKIE) { + Some(ServerAutoReconnect::decode(src)?) + } else { + None + }; + + let errors_info = if present_fields_flags.contains(LogonExFlags::LOGON_ERRORS) { + Some(LogonErrorsInfo::decode(src)?) + } else { + None + }; + + ensure_size!(in: src, size: LOGON_EX_PADDING_SIZE); + read_padding!(src, LOGON_EX_PADDING_SIZE); + + Ok(Self { + present_fields_flags, + auto_reconnect, + errors_info, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerAutoReconnect { + pub logon_id: u32, + pub random_bits: [u8; AUTO_RECONNECT_RANDOM_BITS_SIZE], +} + +impl ServerAutoReconnect { + const NAME: &'static str = "ServerAutoReconnect"; + + const FIXED_PART_SIZE: usize = AUTO_RECONNECT_PACKET_SIZE + LOGON_INFO_FIELD_DATA_SIZE; +} + +impl Encode for ServerAutoReconnect { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(u32::try_from(AUTO_RECONNECT_PACKET_SIZE).expect("AUTO_RECONNECT_PACKET_SIZE fits into u32")); + dst.write_u32(u32::try_from(AUTO_RECONNECT_PACKET_SIZE).expect("AUTO_RECONNECT_PACKET_SIZE fits into u32")); + dst.write_u32(AUTO_RECONNECT_VERSION_1); + dst.write_u32(self.logon_id); + dst.write_slice(self.random_bits.as_ref()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ServerAutoReconnect { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _data_length = src.read_u32(); + let packet_length = src.read_u32(); + if packet_length != u32::try_from(AUTO_RECONNECT_PACKET_SIZE).expect("AUTO_RECONNECT_PACKET_SIZE fits into u32") + { + return Err(invalid_field_err!("packetLen", "invalid auto-reconnect packet size")); + } + + let version = src.read_u32(); + if version != AUTO_RECONNECT_VERSION_1 { + return Err(invalid_field_err!("version", "invalid auto-reconnect version")); + } + + let logon_id = src.read_u32(); + let random_bits = src.read_array(); + + Ok(Self { logon_id, random_bits }) + } +} + +/// TS_LOGON_ERRORS_INFO +/// +/// [Doc](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/845eb789-6edf-453a-8b0e-c976823d1f72) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogonErrorsInfo { + pub error_type: LogonErrorNotificationType, + pub error_data: LogonErrorNotificationData, +} + +impl LogonErrorsInfo { + const NAME: &'static str = "LogonErrorsInfo"; + + const FIXED_PART_SIZE: usize = LOGON_ERRORS_INFO_SIZE + LOGON_INFO_FIELD_DATA_SIZE; +} + +impl Encode for LogonErrorsInfo { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(u32::try_from(LOGON_ERRORS_INFO_SIZE).expect("LOGON_ERRORS_INFO_SIZE fits into u32")); + dst.write_u32(self.error_type.as_u32()); + dst.write_u32(self.error_data.to_u32()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for LogonErrorsInfo { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let _data_length = src.read_u32(); + let error_type = LogonErrorNotificationType::from_u32(src.read_u32()) + .ok_or_else(|| invalid_field_err!("errorType", "invalid logon error type"))?; + + let error_notification_data = src.read_u32(); + let error_data = LogonErrorNotificationDataErrorCode::from_u32(error_notification_data) + .map(LogonErrorNotificationData::ErrorCode) + .unwrap_or(LogonErrorNotificationData::SessionId(error_notification_data)); + + Ok(Self { error_type, error_data }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct LogonExFlags: u32 { + const AUTO_RECONNECT_COOKIE = 0x0000_0001; + const LOGON_ERRORS = 0x0000_0002; + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum LogonErrorNotificationType { + SessionBusyOptions = 0xFFFF_FFF8, + DisconnectRefused = 0xFFFF_FFF9, + NoPermission = 0xFFFF_FFFA, + BumpOptions = 0xFFFF_FFFB, + ReconnectOptions = 0xFFFF_FFFC, + SessionTerminate = 0xFFFF_FFFD, + SessionContinue = 0xFFFF_FFFE, + AccessDenied = 0xFFFF_FFFF, +} + +impl LogonErrorNotificationType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum LogonErrorNotificationDataErrorCode { + FailedBadPassword = 0x0000_0000, + FailedUpdatePassword = 0x0000_0001, + FailedOther = 0x0000_0002, + Warning = 0x0000_0003, +} + +impl LogonErrorNotificationDataErrorCode { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LogonErrorNotificationData { + ErrorCode(LogonErrorNotificationDataErrorCode), + SessionId(u32), +} + +impl LogonErrorNotificationData { + pub fn to_u32(&self) -> u32 { + match self { + LogonErrorNotificationData::ErrorCode(code) => code.as_u32(), + LogonErrorNotificationData::SessionId(id) => *id, + } + } +} diff --git a/crates/ironrdp-pdu/src/rdp/session_info/logon_info.rs b/crates/ironrdp-pdu/src/rdp/session_info/logon_info.rs new file mode 100644 index 00000000..e30c54b2 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/session_info/logon_info.rs @@ -0,0 +1,196 @@ +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, write_padding, Decode, + DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; + +use crate::utils; + +const DOMAIN_NAME_SIZE_FIELD_SIZE: usize = 4; +const DOMAIN_NAME_SIZE_V1: usize = 52; +const USER_NAME_SIZE_FIELD_SIZE: usize = 4; +const USER_NAME_SIZE_V1: usize = 512; +const ID_SESSION_SIZE: usize = 4; + +const SAVE_SESSION_PDU_VERSION_ONE: u16 = 0x0001; +const LOGON_INFO_V2_SIZE: usize = 18; +const LOGON_INFO_V2_PADDING_SIZE: usize = 558; +const DOMAIN_NAME_SIZE_V2: usize = 52; +const USER_NAME_SIZE_V2: usize = 512; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogonInfoVersion1 { + pub logon_info: LogonInfo, +} + +impl LogonInfoVersion1 { + const NAME: &'static str = "LogonInfoVersion1"; + + const FIXED_PART_SIZE: usize = DOMAIN_NAME_SIZE_FIELD_SIZE + + DOMAIN_NAME_SIZE_V1 + + USER_NAME_SIZE_FIELD_SIZE + + USER_NAME_SIZE_V1 + + ID_SESSION_SIZE; +} + +impl Encode for LogonInfoVersion1 { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let mut domain_name_buffer = utils::to_utf16_bytes(self.logon_info.domain_name.as_ref()); + domain_name_buffer.resize(DOMAIN_NAME_SIZE_V1 - 2, 0); + let mut user_name_buffer = utils::to_utf16_bytes(self.logon_info.user_name.as_ref()); + user_name_buffer.resize(USER_NAME_SIZE_V1 - 2, 0); + + dst.write_u32(cast_length!( + "domainNameSize", + (self.logon_info.domain_name.len() + 1) * 2 + )?); + dst.write_slice(domain_name_buffer.as_ref()); + dst.write_u16(0); // UTF-16 null terminator + dst.write_u32(cast_length!("userNameSize", (self.logon_info.user_name.len() + 1) * 2)?); + dst.write_slice(user_name_buffer.as_ref()); + dst.write_u16(0); // UTF-16 null terminator + dst.write_u32(self.logon_info.session_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for LogonInfoVersion1 { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let domain_name_size: usize = cast_length!("domainNameSize", src.read_u32())?; + if domain_name_size > DOMAIN_NAME_SIZE_V1 { + return Err(invalid_field_err!("domainNameSize", "invalid domain name size")); + } + + let domain_name = + utils::decode_string(src.read_slice(DOMAIN_NAME_SIZE_V1), utils::CharacterSet::Unicode, false)?; + + let user_name_size: usize = cast_length!("userNameSize", src.read_u32())?; + if user_name_size > USER_NAME_SIZE_V1 { + return Err(invalid_field_err!("userNameSize", "invalid user name size")); + } + + let user_name = utils::decode_string(src.read_slice(USER_NAME_SIZE_V1), utils::CharacterSet::Unicode, false)?; + + let session_id = src.read_u32(); + + Ok(Self { + logon_info: LogonInfo { + session_id, + domain_name, + user_name, + }, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogonInfoVersion2 { + pub logon_info: LogonInfo, +} + +impl LogonInfoVersion2 { + const NAME: &'static str = "LogonInfoVersion2"; + + const FIXED_PART_SIZE: usize = LOGON_INFO_V2_SIZE + LOGON_INFO_V2_PADDING_SIZE; +} + +impl Encode for LogonInfoVersion2 { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(SAVE_SESSION_PDU_VERSION_ONE); + dst.write_u32(u32::try_from(LOGON_INFO_V2_SIZE).expect("LOGON_INFO_V2_SIZE fits into u32")); + dst.write_u32(self.logon_info.session_id); + dst.write_u32(cast_length!( + "domainNameSize", + (self.logon_info.domain_name.len() + 1) * 2 + )?); + dst.write_u32(cast_length!("userNameSize", (self.logon_info.user_name.len() + 1) * 2)?); + write_padding!(dst, LOGON_INFO_V2_PADDING_SIZE); + + utils::write_string_to_cursor( + dst, + self.logon_info.domain_name.as_ref(), + utils::CharacterSet::Unicode, + true, + )?; + utils::write_string_to_cursor( + dst, + self.logon_info.user_name.as_ref(), + utils::CharacterSet::Unicode, + true, + )?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + (self.logon_info.domain_name.len() + 1) * 2 + (self.logon_info.user_name.len() + 1) * 2 + } +} + +impl<'de> Decode<'de> for LogonInfoVersion2 { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version = src.read_u16(); + if version != SAVE_SESSION_PDU_VERSION_ONE { + return Err(invalid_field_err!("version", "invalid logon version 2")); + } + + let size: usize = cast_length!("LogonInfoSize", src.read_u32())?; + if size != LOGON_INFO_V2_SIZE { + return Err(invalid_field_err!("domainNameSize", "invalid logon info size")); + } + + let session_id = src.read_u32(); + let domain_name_size: usize = cast_length!("domainNameSize", src.read_u32())?; + if domain_name_size > DOMAIN_NAME_SIZE_V2 { + return Err(invalid_field_err!("domainNameSize", "invalid domain name size")); + } + + let user_name_size: usize = cast_length!("userNameSize", src.read_u32())?; + if user_name_size > USER_NAME_SIZE_V2 { + return Err(invalid_field_err!("userNameSize", "invalid user name size")); + } + + read_padding!(src, LOGON_INFO_V2_PADDING_SIZE); + + ensure_size!(in: src, size: domain_name_size); + let domain_name = utils::decode_string(src.read_slice(domain_name_size), utils::CharacterSet::Unicode, false)?; + + ensure_size!(in: src, size: user_name_size); + let user_name = utils::decode_string(src.read_slice(user_name_size), utils::CharacterSet::Unicode, false)?; + + Ok(Self { + logon_info: LogonInfo { + session_id, + domain_name, + user_name, + }, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogonInfo { + pub session_id: u32, + pub user_name: String, + pub domain_name: String, +} diff --git a/crates/ironrdp-pdu/src/rdp/session_info/mod.rs b/crates/ironrdp-pdu/src/rdp/session_info/mod.rs new file mode 100644 index 00000000..b64c8869 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/session_info/mod.rs @@ -0,0 +1,160 @@ +use std::io; + +use ironrdp_core::{ + ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, write_padding, Decode, DecodeResult, Encode, + EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; +use thiserror::Error; + +use crate::PduError; + +#[cfg(test)] +mod tests; + +mod logon_extended; +mod logon_info; + +pub use self::logon_extended::{ + LogonErrorNotificationData, LogonErrorNotificationDataErrorCode, LogonErrorNotificationType, LogonErrorsInfo, + LogonExFlags, LogonInfoExtended, ServerAutoReconnect, +}; +pub use self::logon_info::{LogonInfo, LogonInfoVersion1, LogonInfoVersion2}; + +const INFO_TYPE_FIELD_SIZE: usize = 4; +const PLAIN_NOTIFY_PADDING_SIZE: usize = 576; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SaveSessionInfoPdu { + pub info_type: InfoType, + pub info_data: InfoData, +} + +impl SaveSessionInfoPdu { + const NAME: &'static str = "SaveSessionInfoPdu"; + + const FIXED_PART_SIZE: usize = INFO_TYPE_FIELD_SIZE; +} + +impl Encode for SaveSessionInfoPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.info_type.as_u32()); + match self.info_data { + InfoData::LogonInfoV1(ref info_v1) => { + info_v1.encode(dst)?; + } + InfoData::LogonInfoV2(ref info_v2) => { + info_v2.encode(dst)?; + } + InfoData::PlainNotify => { + ensure_size!(in: dst, size: PLAIN_NOTIFY_PADDING_SIZE); + write_padding!(dst, PLAIN_NOTIFY_PADDING_SIZE); + } + InfoData::LogonExtended(ref extended) => { + extended.encode(dst)?; + } + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let info_data_size = match self.info_data { + InfoData::LogonInfoV1(ref info_v1) => info_v1.size(), + InfoData::LogonInfoV2(ref info_v2) => info_v2.size(), + InfoData::PlainNotify => PLAIN_NOTIFY_PADDING_SIZE, + InfoData::LogonExtended(ref extended) => extended.size(), + }; + + Self::FIXED_PART_SIZE + info_data_size + } +} + +impl<'de> Decode<'de> for SaveSessionInfoPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let info_type = InfoType::from_u32(src.read_u32()) + .ok_or_else(|| invalid_field_err!("infoType", "invalid save session info type"))?; + + let info_data = match info_type { + InfoType::Logon => InfoData::LogonInfoV1(LogonInfoVersion1::decode(src)?), + InfoType::LogonLong => InfoData::LogonInfoV2(LogonInfoVersion2::decode(src)?), + InfoType::PlainNotify => { + ensure_size!(in: src, size: PLAIN_NOTIFY_PADDING_SIZE); + read_padding!(src, PLAIN_NOTIFY_PADDING_SIZE); + + InfoData::PlainNotify + } + InfoType::LogonExtended => InfoData::LogonExtended(LogonInfoExtended::decode(src)?), + }; + + Ok(Self { info_type, info_data }) + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum InfoType { + Logon = 0x0000_0000, + LogonLong = 0x0000_0001, + PlainNotify = 0x0000_0002, + LogonExtended = 0x0000_0003, +} + +impl InfoType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InfoData { + LogonInfoV1(LogonInfoVersion1), + LogonInfoV2(LogonInfoVersion2), + PlainNotify, + LogonExtended(LogonInfoExtended), +} + +#[derive(Debug, Error)] +pub enum SessionError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("invalid save session info type value")] + InvalidSaveSessionInfoType, + #[error("invalid domain name size value")] + InvalidDomainNameSize, + #[error("invalid user name size value")] + InvalidUserNameSize, + #[error("invalid logon version value")] + InvalidLogonVersion2, + #[error("invalid logon info version2 size value")] + InvalidLogonVersion2Size, + #[error("invalid server auto-reconnect packet size value")] + InvalidAutoReconnectPacketSize, + #[error("invalid server auto-reconnect version")] + InvalidAutoReconnectVersion, + #[error("invalid logon error type value")] + InvalidLogonErrorType, + #[error("invalid logon error data value")] + InvalidLogonErrorData, + #[error("PDU error: {0}")] + Pdu(PduError), +} + +impl From for SessionError { + fn from(e: PduError) -> Self { + Self::Pdu(e) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/session_info/tests.rs b/crates/ironrdp-pdu/src/rdp/session_info/tests.rs new file mode 100644 index 00000000..2af92759 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/session_info/tests.rs @@ -0,0 +1,434 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec, DecodeErrorKind}; + +use super::*; + +const LOGON_INFO_V1_BUFFER: [u8; 576] = [ + 0x0c, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x44, 0x00, 0x45, 0x00, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, + 0x00, 0x00, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, +]; + +const LOGON_INFO_V2_BUFFER: [u8; 602] = [ + 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x44, 0x00, 0x45, 0x00, 0x56, 0x00, 0x00, 0x00, 0x65, + 0x00, 0x6c, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x00, 0x00, +]; + +const SESSION_PLAIN_NOTIFY_BUFFER: [u8; 580] = [ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +const LOGON_EXTENDED_BUFFER: [u8; 620] = [ + 0x32, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0xa8, 0x02, 0xe7, 0x25, 0xe2, 0x4c, 0x82, 0xb7, 0x52, 0xa5, 0x53, 0x50, 0x34, 0x98, 0xa1, 0xa8, + 0x08, 0x00, 0x00, 0x00, 0xfa, 0xff, 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +const INVALID_SESSION_INFO_TYPE_BUFFER: [u8; 4] = [0x04, 0x00, 0x00, 0x00]; +const LOGON_INFO_V1_WITH_INVALID_DOMAIN_SIZE_BUFFER: [u8; 576] = [ + 0x35, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x44, 0x00, 0x45, 0x00, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, + 0x00, 0x00, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, +]; + +const LOGON_INFO_V1_WITH_INVALID_USER_NAME_SIZE_BUFFER: [u8; 576] = [ + 0x0c, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x44, 0x00, 0x45, 0x00, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x02, 0x00, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, +]; + +const LOGON_INFO_V2_WITH_INVALID_LOGON_VERSION_BUFFER: [u8; 2] = [0x00, 0x00]; +const LOGON_INFO_V2_WITH_INVALID_LOGON_INFO_V2_SIZE_BUFFER: [u8; 6] = [0x01, 0x00, 0x13, 0x00, 0x00, 0x00]; + +const LOGON_EXTENDED_WITH_INVALID_RECONNECT_PACKET_SIZE_BUFFER: [u8; 28] = [ + 0x26, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +const LOGON_EXTENDED_WITH_INVALID_RECONNECT_VERSION_BUFFER: [u8; 38] = [ + 0x26, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +const LOGON_EXTENDED_WITH_INVALID_LOGON_ERROR_TYPE_BUFFER: [u8; 50] = [ + 0x32, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0xa8, 0x02, 0xe7, 0x25, 0xe2, 0x4c, 0x82, 0xb7, 0x52, 0xa5, 0x53, 0x50, 0x34, 0x98, 0xa1, 0xa8, + 0x08, 0x00, 0x00, 0x00, 0xf0, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, +]; + +const DOMAIN_NAME: &str = "NTDEV"; +const USER_NAME: &str = "eltons"; +const SESSION_ID: u32 = 0x02; + +static LOGON_INFO_V1: LazyLock = LazyLock::new(|| LogonInfoVersion1 { + logon_info: LogonInfo { + domain_name: DOMAIN_NAME.to_owned(), + user_name: USER_NAME.to_owned(), + session_id: SESSION_ID, + }, +}); +static LOGON_INFO_V2: LazyLock = LazyLock::new(|| LogonInfoVersion2 { + logon_info: LogonInfo { + domain_name: DOMAIN_NAME.to_owned(), + user_name: USER_NAME.to_owned(), + session_id: SESSION_ID, + }, +}); +static LOGON_EXTENDED: LazyLock = LazyLock::new(|| LogonInfoExtended { + present_fields_flags: LogonExFlags::AUTO_RECONNECT_COOKIE | LogonExFlags::LOGON_ERRORS, + auto_reconnect: Some(ServerAutoReconnect { + logon_id: SESSION_ID, + random_bits: [ + 0xa8, 0x02, 0xe7, 0x25, 0xe2, 0x4c, 0x82, 0xb7, 0x52, 0xa5, 0x53, 0x50, 0x34, 0x98, 0xa1, 0xa8, + ], + }), + errors_info: Some(LogonErrorsInfo { + error_type: LogonErrorNotificationType::NoPermission, + error_data: LogonErrorNotificationData::ErrorCode(LogonErrorNotificationDataErrorCode::FailedOther), + }), +}); +static SESSION_PLAIN_NOTIFY: LazyLock = LazyLock::new(|| SaveSessionInfoPdu { + info_type: InfoType::PlainNotify, + info_data: InfoData::PlainNotify, +}); + +#[test] +fn from_buffer_correct_parses_logon_info_v1() { + assert_eq!(LOGON_INFO_V1.clone(), decode(LOGON_INFO_V1_BUFFER.as_ref()).unwrap(),); +} + +#[test] +fn to_buffer_correct_serializes_logon_info_v1() { + let info_v1 = LOGON_INFO_V1.clone(); + + let buffer = encode_vec(&info_v1).unwrap(); + + assert_eq!(LOGON_INFO_V1_BUFFER.as_ref(), buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_logon_info_v1() { + let info_v1 = LOGON_INFO_V1.clone(); + let expected_buf_len = LOGON_INFO_V1_BUFFER.len(); + + let len = info_v1.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn from_buffer_correct_parses_logon_info_v2() { + assert_eq!(LOGON_INFO_V2.clone(), decode(LOGON_INFO_V2_BUFFER.as_ref()).unwrap(),); +} + +#[test] +fn to_buffer_correct_serializes_logon_info_v2() { + let info_v2 = LOGON_INFO_V2.clone(); + + let buffer = encode_vec(&info_v2).unwrap(); + + assert_eq!(LOGON_INFO_V2_BUFFER.as_ref(), buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_logon_info_v2() { + let info_v2 = LOGON_INFO_V2.clone(); + let expected_buf_len = LOGON_INFO_V2_BUFFER.len(); + + let len = info_v2.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn from_buffer_correct_parses_plain_notify() { + match decode::(SESSION_PLAIN_NOTIFY_BUFFER.as_ref()) + .unwrap() + .info_data + { + InfoData::PlainNotify => {} + _ => panic!("Unexpected SaveSessionInfoPdu data"), + } +} + +#[test] +fn to_buffer_correct_serializes_plain_notify() { + let session_plain_notify = SESSION_PLAIN_NOTIFY.clone(); + + let buffer = encode_vec(&session_plain_notify).unwrap(); + + assert_eq!(SESSION_PLAIN_NOTIFY_BUFFER.as_ref(), buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_plain_notify() { + let session_plain_notify = SESSION_PLAIN_NOTIFY.clone(); + let expected_buf_len = SESSION_PLAIN_NOTIFY_BUFFER.len(); + + let len = session_plain_notify.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn from_buffer_correct_parses_extended_info() { + assert_eq!(LOGON_EXTENDED.clone(), decode(LOGON_EXTENDED_BUFFER.as_ref()).unwrap(),); +} + +#[test] +fn to_buffer_correct_serializes_extended_info() { + let extended = LOGON_EXTENDED.clone(); + + let buffer = encode_vec(&extended).unwrap(); + + assert_eq!(LOGON_EXTENDED_BUFFER.as_ref(), buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_extended_info() { + let extended = LOGON_EXTENDED.clone(); + let expected_buf_len = LOGON_EXTENDED_BUFFER.len(); + + let len = extended.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn from_buffer_parsing_with_invalid_session_info_type_fails() { + match decode::(INVALID_SESSION_INFO_TYPE_BUFFER.as_ref()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::InvalidField { .. }) => (), + res => panic!("Expected InvalidSaveSessionInfoType error, got: {res:?}"), + }; +} + +#[test] +fn from_buffer_parsing_with_invalid_domain_size_fails() { + match decode::(LOGON_INFO_V1_WITH_INVALID_DOMAIN_SIZE_BUFFER.as_ref()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::InvalidField { .. }) => (), + res => panic!("Expected InvalidDomainNameSize error, got: {res:?}"), + }; +} + +#[test] +fn from_buffer_parsing_with_invalid_user_name_size_fails() { + match decode::(LOGON_INFO_V1_WITH_INVALID_USER_NAME_SIZE_BUFFER.as_ref()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::InvalidField { .. }) => (), + res => panic!("Expected InvalidUserNameSize error, got: {res:?}"), + }; +} + +#[test] +fn from_buffer_parsing_with_invalid_logon_version_fails() { + match decode::(LOGON_INFO_V2_WITH_INVALID_LOGON_VERSION_BUFFER.as_ref()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::NotEnoughBytes { .. }) => (), + res => panic!("Expected InvalidLogonVersion2 error, got: {res:?}"), + }; +} + +#[test] +fn from_buffer_parsing_with_invalid_logon_infov2_size_fails() { + match decode::(LOGON_INFO_V2_WITH_INVALID_LOGON_INFO_V2_SIZE_BUFFER.as_ref()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::NotEnoughBytes { .. }) => (), + res => panic!("Expected InvalidLogonVersion2Size error, got: {res:?}"), + }; +} + +#[test] +fn from_buffer_parsing_with_invalid_reconnect_packet_size_fails() { + match decode::(LOGON_EXTENDED_WITH_INVALID_RECONNECT_PACKET_SIZE_BUFFER.as_ref()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::NotEnoughBytes { .. }) => (), + res => panic!("Expected InvalidAutoReconnectPacketSize error, got: {res:?}"), + }; +} + +#[test] +fn from_buffer_parsing_with_invalid_reconnect_version_fails() { + match decode::(LOGON_EXTENDED_WITH_INVALID_RECONNECT_VERSION_BUFFER.as_ref()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::InvalidField { .. }) => (), + res => panic!("Expected InvalidAutoReconnectVersion error, got: {res:?}"), + }; +} + +#[test] +fn from_buffer_parsing_with_invalid_logon_error_type_fails() { + match decode::(LOGON_EXTENDED_WITH_INVALID_LOGON_ERROR_TYPE_BUFFER.as_ref()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::InvalidField { .. }) => (), + res => panic!("Expected InvalidLogonErrorType error, got: {res:?}"), + }; +} diff --git a/crates/ironrdp-pdu/src/rdp/suppress_output.rs b/crates/ironrdp-pdu/src/rdp/suppress_output.rs new file mode 100644 index 00000000..684e7393 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/suppress_output.rs @@ -0,0 +1,96 @@ +use ironrdp_core::{ + ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, write_padding, Decode, DecodeResult, Encode, + EncodeResult, ReadCursor, WriteCursor, +}; + +use crate::geometry::InclusiveRectangle; + +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum AllowDisplayUpdatesType { + SuppressDisplayUpdates = 0x00, + AllowDisplayUpdates = 0x01, +} + +impl AllowDisplayUpdatesType { + pub fn from_u8(value: u8) -> Option { + match value { + 0x00 => Some(Self::SuppressDisplayUpdates), + 0x01 => Some(Self::AllowDisplayUpdates), + _ => None, + } + } + + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +/// [2.2.11.3.1] Suppress Output PDU Data (TS_SUPPRESS_OUTPUT_PDU) +/// +/// The Suppress Output PDU is sent by the client to toggle all display updates +/// from the server. This packet does not end the session or socket connection. +/// Typically, a client sends this packet when its window is either minimized or +/// restored. Server support for this PDU is indicated in the General Capability +/// Set [2.2.7.1.1]. +/// +/// [2.2.11.3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/0be71491-0b01-402c-947d-080706ccf91b +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SuppressOutputPdu { + pub desktop_rect: Option, +} + +impl SuppressOutputPdu { + const NAME: &'static str = "SuppressOutputPdu"; + + const FIXED_PART_SIZE: usize = 1 /* allowDisplayUpdates */ + 3 /* pad */; +} + +impl Encode for SuppressOutputPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let allow_display_updates = if self.desktop_rect.is_some() { + AllowDisplayUpdatesType::AllowDisplayUpdates + } else { + AllowDisplayUpdatesType::SuppressDisplayUpdates + }; + + dst.write_u8(allow_display_updates.as_u8()); + write_padding!(dst, 3); + if let Some(rect) = &self.desktop_rect { + rect.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.desktop_rect.as_ref().map_or(0, |r| r.size()) + // desktopRect + } +} + +impl<'de> Decode<'de> for SuppressOutputPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let allow_display_updates = AllowDisplayUpdatesType::from_u8(src.read_u8()) + .ok_or_else(|| invalid_field_err!("allowDisplayUpdates", "invalid display update type"))?; + read_padding!(src, 3); + let desktop_rect = if allow_display_updates == AllowDisplayUpdatesType::AllowDisplayUpdates { + Some(InclusiveRectangle::decode(src)?) + } else { + None + }; + Ok(Self { desktop_rect }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/avc_messages.rs b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/avc_messages.rs new file mode 100644 index 00000000..73b17078 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/avc_messages.rs @@ -0,0 +1,220 @@ +use core::fmt::Debug; + +use bit_field::BitField as _; +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; + +use crate::geometry::InclusiveRectangle; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct QuantQuality { + pub quantization_parameter: u8, + pub progressive: bool, + pub quality: u8, +} + +impl QuantQuality { + const NAME: &'static str = "GfxQuantQuality"; + + const FIXED_PART_SIZE: usize = 1 /* data */ + 1 /* quality */; +} + +impl Encode for QuantQuality { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let mut data = 0u8; + data.set_bits(0..6, self.quantization_parameter); + data.set_bit(7, self.progressive); + dst.write_u8(data); + dst.write_u8(self.quality); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for QuantQuality { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let data = src.read_u8(); + let qp = data.get_bits(0..6); + let progressive = data.get_bit(7); + let quality = src.read_u8(); + Ok(QuantQuality { + quantization_parameter: qp, + progressive, + quality, + }) + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct Avc420BitmapStream<'a> { + pub rectangles: Vec, + pub quant_qual_vals: Vec, + pub data: &'a [u8], +} + +impl Debug for Avc420BitmapStream<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Avc420BitmapStream") + .field("rectangles", &self.rectangles) + .field("quant_qual_vals", &self.quant_qual_vals) + .field("data_len", &self.data.len()) + .finish() + } +} + +impl Avc420BitmapStream<'_> { + const NAME: &'static str = "Avc420BitmapStream"; + + const FIXED_PART_SIZE: usize = 4 /* nRect */; +} + +impl Encode for Avc420BitmapStream<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(cast_length!("len", self.rectangles.len())?); + for rectangle in &self.rectangles { + rectangle.encode(dst)?; + } + for quant_qual_val in &self.quant_qual_vals { + quant_qual_val.encode(dst)?; + } + dst.write_slice(self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + // Each rectangle is 8 bytes and 2 bytes for each quant val + Self::FIXED_PART_SIZE + self.rectangles.len() * 10 + self.data.len() + } +} + +impl<'de> Decode<'de> for Avc420BitmapStream<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let num_regions = cast_length!("number of regions", src.read_u32())?; + let mut rectangles = Vec::with_capacity(num_regions); + let mut quant_qual_vals = Vec::with_capacity(num_regions); + for _ in 0..num_regions { + rectangles.push(InclusiveRectangle::decode(src)?); + } + for _ in 0..num_regions { + quant_qual_vals.push(QuantQuality::decode(src)?); + } + let data = src.remaining(); + Ok(Avc420BitmapStream { + rectangles, + quant_qual_vals, + data, + }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Encoding: u8 { + const LUMA_AND_CHROMA = 0x00; + const LUMA = 0x01; + const CHROMA = 0x02; + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Avc444BitmapStream<'a> { + pub encoding: Encoding, + pub stream1: Avc420BitmapStream<'a>, + pub stream2: Option>, +} + +impl Avc444BitmapStream<'_> { + const NAME: &'static str = "Avc444BitmapStream"; + + const FIXED_PART_SIZE: usize = 4 /* streamInfo */; +} + +impl Encode for Avc444BitmapStream<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let mut stream_info = 0u32; + stream_info.set_bits(0..30, cast_length!("stream1size", self.stream1.size())?); + stream_info.set_bits(30..32, u32::from(self.encoding.bits())); + dst.write_u32(stream_info); + self.stream1.encode(dst)?; + if let Some(stream) = self.stream2.as_ref() { + stream.encode(dst)?; + } + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let stream2_size = if let Some(stream) = self.stream2.as_ref() { + stream.size() + } else { + 0 + }; + + Self::FIXED_PART_SIZE + self.stream1.size() + stream2_size + } +} + +impl<'de> Decode<'de> for Avc444BitmapStream<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let stream_info = src.read_u32(); + let stream_len = stream_info.get_bits(0..30); + let encoding = + Encoding::from_bits_truncate(u8::try_from(stream_info.get_bits(30..32)).expect("value fits into u8")); + + if stream_len == 0 { + if encoding == Encoding::LUMA_AND_CHROMA { + return Err(invalid_field_err!("encoding", "invalid encoding")); + } + + let stream1 = Avc420BitmapStream::decode(src)?; + Ok(Avc444BitmapStream { + encoding, + stream1, + stream2: None, + }) + } else { + let (mut stream1, mut stream2) = src.split_at(cast_length!("first stream length", stream_len)?); + let stream1 = Avc420BitmapStream::decode(&mut stream1)?; + let stream2 = if encoding == Encoding::LUMA_AND_CHROMA { + Some(Avc420BitmapStream::decode(&mut stream2)?) + } else { + None + }; + Ok(Avc444BitmapStream { + encoding, + stream1, + stream2, + }) + } + } +} diff --git a/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/client.rs b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/client.rs new file mode 100644 index 00000000..c2fee592 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/client.rs @@ -0,0 +1,175 @@ +use core::iter; + +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +use super::CapabilitySet; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CapabilitiesAdvertisePdu(pub Vec); + +impl CapabilitiesAdvertisePdu { + const NAME: &'static str = "CapabilitiesAdvertisePdu"; + + const FIXED_PART_SIZE: usize = 2 /* Count */; +} + +impl Encode for CapabilitiesAdvertisePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(cast_length!("Count", self.0.len())?); + + for capability_set in self.0.iter() { + capability_set.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.0.iter().map(|c| c.size()).sum::() + } +} + +impl<'a> Decode<'a> for CapabilitiesAdvertisePdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let capabilities_count = cast_length!("Count", src.read_u16())?; + + ensure_size!(in: src, size: capabilities_count * CapabilitySet::FIXED_PART_SIZE); + + let capabilities = iter::repeat_with(|| CapabilitySet::decode(src)) + .take(capabilities_count) + .collect::>()?; + + Ok(Self(capabilities)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FrameAcknowledgePdu { + pub queue_depth: QueueDepth, + pub frame_id: u32, + pub total_frames_decoded: u32, +} + +impl FrameAcknowledgePdu { + const NAME: &'static str = "FrameAcknowledgePdu"; + + const FIXED_PART_SIZE: usize = 4 /* QueueDepth */ + 4 /* FrameId */ + 4 /* TotalFramesDecoded */; +} + +impl Encode for FrameAcknowledgePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.queue_depth.to_u32()); + dst.write_u32(self.frame_id); + dst.write_u32(self.total_frames_decoded); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for FrameAcknowledgePdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let queue_depth = QueueDepth::from_u32(src.read_u32()); + let frame_id = src.read_u32(); + let total_frames_decoded = src.read_u32(); + + Ok(Self { + queue_depth, + frame_id, + total_frames_decoded, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CacheImportReplyPdu { + pub cache_slots: Vec, +} + +impl CacheImportReplyPdu { + const NAME: &'static str = "CacheImportReplyPdu"; + + const FIXED_PART_SIZE: usize = 2 /* Count */; +} + +impl Encode for CacheImportReplyPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(cast_length!("Count", self.cache_slots.len())?); + + for cache_slot in self.cache_slots.iter() { + dst.write_u16(*cache_slot); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.cache_slots.iter().map(|_| 2).sum::() + } +} + +impl<'a> Decode<'a> for CacheImportReplyPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let entries_count = usize::from(src.read_u16()); + + let cache_slots = iter::repeat_with(|| src.read_u16()).take(entries_count).collect(); + + Ok(Self { cache_slots }) + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum QueueDepth { + Unavailable, + AvailableBytes(u32), + Suspend, +} + +impl QueueDepth { + pub fn from_u32(v: u32) -> Self { + match v { + 0x0000_0000 => Self::Unavailable, + 0x0000_0001..=0xFFFF_FFFE => Self::AvailableBytes(v), + 0xFFFF_FFFF => Self::Suspend, + } + } + + pub fn to_u32(self) -> u32 { + match self { + Self::Unavailable => 0x0000_0000, + Self::AvailableBytes(v) => v, + Self::Suspend => 0xFFFF_FFFF, + } + } +} diff --git a/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/mod.rs b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/mod.rs new file mode 100644 index 00000000..62cf393e --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/mod.rs @@ -0,0 +1,352 @@ +mod client; +mod server; + +mod avc_messages; +use bitflags::bitflags; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +#[rustfmt::skip] // do not re-order this +pub use avc_messages::{Avc420BitmapStream, Avc444BitmapStream, Encoding, QuantQuality}; +pub use client::{CacheImportReplyPdu, CapabilitiesAdvertisePdu, FrameAcknowledgePdu, QueueDepth}; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +pub use server::{ + CacheToSurfacePdu, CapabilitiesConfirmPdu, Codec1Type, Codec2Type, CreateSurfacePdu, DeleteEncodingContextPdu, + DeleteSurfacePdu, EndFramePdu, EvictCacheEntryPdu, MapSurfaceToOutputPdu, MapSurfaceToScaledOutputPdu, + MapSurfaceToScaledWindowPdu, PixelFormat, ResetGraphicsPdu, SolidFillPdu, StartFramePdu, SurfaceToCachePdu, + SurfaceToSurfacePdu, Timestamp, WireToSurface1Pdu, WireToSurface2Pdu, +}; + +use super::RDP_GFX_HEADER_SIZE; + +const CAPABILITY_SET_HEADER_SIZE: usize = 8; + +const V10_1_RESERVED: u128 = 0; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CapabilitySet { + V8 { flags: CapabilitiesV8Flags }, + V8_1 { flags: CapabilitiesV81Flags }, + V10 { flags: CapabilitiesV10Flags }, + V10_1, + V10_2 { flags: CapabilitiesV10Flags }, + V10_3 { flags: CapabilitiesV103Flags }, + V10_4 { flags: CapabilitiesV104Flags }, + V10_5 { flags: CapabilitiesV104Flags }, + V10_6 { flags: CapabilitiesV104Flags }, + V10_6Err { flags: CapabilitiesV104Flags }, + V10_7 { flags: CapabilitiesV107Flags }, + Unknown(Vec), +} + +impl CapabilitySet { + const NAME: &'static str = "GfxCapabilitySet"; + + const FIXED_PART_SIZE: usize = CAPABILITY_SET_HEADER_SIZE; + + fn version(&self) -> CapabilityVersion { + match self { + CapabilitySet::V8 { .. } => CapabilityVersion::V8, + CapabilitySet::V8_1 { .. } => CapabilityVersion::V8_1, + CapabilitySet::V10 { .. } => CapabilityVersion::V10, + CapabilitySet::V10_1 => CapabilityVersion::V10_1, + CapabilitySet::V10_2 { .. } => CapabilityVersion::V10_2, + CapabilitySet::V10_3 { .. } => CapabilityVersion::V10_3, + CapabilitySet::V10_4 { .. } => CapabilityVersion::V10_4, + CapabilitySet::V10_5 { .. } => CapabilityVersion::V10_5, + CapabilitySet::V10_6 { .. } => CapabilityVersion::V10_6, + CapabilitySet::V10_6Err { .. } => CapabilityVersion::V10_6Err, + CapabilitySet::V10_7 { .. } => CapabilityVersion::V10_7, + CapabilitySet::Unknown { .. } => CapabilityVersion::Unknown, + } + } +} + +impl Encode for CapabilitySet { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.version().as_u32()); + dst.write_u32(cast_length!("dataLength", self.size() - CAPABILITY_SET_HEADER_SIZE)?); + + match self { + CapabilitySet::V8 { flags } => dst.write_u32(flags.bits()), + CapabilitySet::V8_1 { flags } => dst.write_u32(flags.bits()), + CapabilitySet::V10 { flags } => dst.write_u32(flags.bits()), + CapabilitySet::V10_1 => dst.write_u128(V10_1_RESERVED), + CapabilitySet::V10_2 { flags } => dst.write_u32(flags.bits()), + CapabilitySet::V10_3 { flags } => dst.write_u32(flags.bits()), + CapabilitySet::V10_4 { flags } => dst.write_u32(flags.bits()), + CapabilitySet::V10_5 { flags } => dst.write_u32(flags.bits()), + CapabilitySet::V10_6 { flags } => dst.write_u32(flags.bits()), + CapabilitySet::V10_6Err { flags } => dst.write_u32(flags.bits()), + CapabilitySet::V10_7 { flags } => dst.write_u32(flags.bits()), + CapabilitySet::Unknown(data) => dst.write_slice(data), + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + CAPABILITY_SET_HEADER_SIZE + + match self { + CapabilitySet::V8 { .. } + | CapabilitySet::V8_1 { .. } + | CapabilitySet::V10 { .. } + | CapabilitySet::V10_2 { .. } + | CapabilitySet::V10_3 { .. } + | CapabilitySet::V10_4 { .. } + | CapabilitySet::V10_5 { .. } + | CapabilitySet::V10_6 { .. } + | CapabilitySet::V10_6Err { .. } + | CapabilitySet::V10_7 { .. } => 4, + CapabilitySet::V10_1 => 16, + CapabilitySet::Unknown(data) => data.len(), + } + } +} + +impl<'de> Decode<'de> for CapabilitySet { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version = CapabilityVersion::from_u32(src.read_u32()) + .ok_or_else(|| invalid_field_err!("version", "unhandled version"))?; + let data_length: usize = cast_length!("dataLength", src.read_u32())?; + + ensure_size!(in: src, size: data_length); + let data = src.read_slice(data_length); + let mut cur = ReadCursor::new(data); + + let size = match version { + CapabilityVersion::V8 + | CapabilityVersion::V8_1 + | CapabilityVersion::V10 + | CapabilityVersion::V10_2 + | CapabilityVersion::V10_3 + | CapabilityVersion::V10_4 + | CapabilityVersion::V10_5 + | CapabilityVersion::V10_6 + | CapabilityVersion::V10_6Err + | CapabilityVersion::V10_7 => 4, + CapabilityVersion::V10_1 => 16, + CapabilityVersion::Unknown => 0, + }; + + ensure_size!(in: cur, size: size); + match version { + CapabilityVersion::V8 => Ok(CapabilitySet::V8 { + flags: CapabilitiesV8Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::V8_1 => Ok(CapabilitySet::V8_1 { + flags: CapabilitiesV81Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::V10 => Ok(CapabilitySet::V10 { + flags: CapabilitiesV10Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::V10_1 => { + cur.read_u128(); + + Ok(CapabilitySet::V10_1) + } + CapabilityVersion::V10_2 => Ok(CapabilitySet::V10_2 { + flags: CapabilitiesV10Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::V10_3 => Ok(CapabilitySet::V10_3 { + flags: CapabilitiesV103Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::V10_4 => Ok(CapabilitySet::V10_4 { + flags: CapabilitiesV104Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::V10_5 => Ok(CapabilitySet::V10_5 { + flags: CapabilitiesV104Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::V10_6 => Ok(CapabilitySet::V10_6 { + flags: CapabilitiesV104Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::V10_6Err => Ok(CapabilitySet::V10_6Err { + flags: CapabilitiesV104Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::V10_7 => Ok(CapabilitySet::V10_7 { + flags: CapabilitiesV107Flags::from_bits_truncate(cur.read_u32()), + }), + CapabilityVersion::Unknown => Ok(CapabilitySet::Unknown(data.to_vec())), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Color { + pub b: u8, + pub g: u8, + pub r: u8, + pub xa: u8, +} + +impl Color { + const NAME: &'static str = "GfxColor"; + + const FIXED_PART_SIZE: usize = 4 /* BGRA */; +} + +impl Encode for Color { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u8(self.b); + dst.write_u8(self.g); + dst.write_u8(self.r); + dst.write_u8(self.xa); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Color { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let b = src.read_u8(); + let g = src.read_u8(); + let r = src.read_u8(); + let xa = src.read_u8(); + + Ok(Self { b, g, r, xa }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Point { + pub x: u16, + pub y: u16, +} + +impl Point { + const NAME: &'static str = "GfxPoint"; + + const FIXED_PART_SIZE: usize = 2 /* X */ + 2 /* Y */; +} + +impl Encode for Point { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.x); + dst.write_u16(self.y); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for Point { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let x = src.read_u16(); + let y = src.read_u16(); + + Ok(Self { x, y }) + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub(crate) enum CapabilityVersion { + V8 = 0x8_0004, + V8_1 = 0x8_0105, + V10 = 0xa_0002, + V10_1 = 0xa_0100, + V10_2 = 0xa_0200, + V10_3 = 0xa_0301, + V10_4 = 0xa_0400, + V10_5 = 0xa_0502, + V10_6 = 0xa_0600, // [MS-RDPEGFX-errata] + V10_6Err = 0xa_0601, // defined similar to FreeRDP to maintain best compatibility + V10_7 = 0xa_0701, + Unknown = 0xa_0702, +} + +impl CapabilityVersion { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u32(self) -> u32 { + self as u32 + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CapabilitiesV8Flags: u32 { + const THIN_CLIENT = 0x1; + const SMALL_CACHE = 0x2; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CapabilitiesV81Flags: u32 { + const THIN_CLIENT = 0x01; + const SMALL_CACHE = 0x02; + const AVC420_ENABLED = 0x10; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CapabilitiesV10Flags: u32 { + const SMALL_CACHE = 0x02; + const AVC_DISABLED = 0x20; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CapabilitiesV103Flags: u32 { + const AVC_DISABLED = 0x20; + const AVC_THIN_CLIENT = 0x40; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CapabilitiesV104Flags: u32 { + const SMALL_CACHE = 0x02; + const AVC_DISABLED = 0x20; + const AVC_THIN_CLIENT = 0x40; + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct CapabilitiesV107Flags: u32 { + const SMALL_CACHE = 0x02; + const AVC_DISABLED = 0x20; + const AVC_THIN_CLIENT = 0x40; + const SCALEDMAP_DISABLE = 0x80; + } +} diff --git a/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/server.rs b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/server.rs new file mode 100644 index 00000000..4684d0a9 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/server.rs @@ -0,0 +1,1045 @@ +use core::iter; +use std::fmt; + +use bit_field::BitField as _; +use ironrdp_core::{ + cast_length, decode_cursor, ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, write_padding, + Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use super::{CapabilitySet, Color, Point, RDP_GFX_HEADER_SIZE}; +use crate::gcc::Monitor; +use crate::geometry::InclusiveRectangle; + +pub(crate) const RESET_GRAPHICS_PDU_SIZE: usize = 340; + +const MAX_RESET_GRAPHICS_WIDTH_HEIGHT: u32 = 32_766; +const MONITOR_COUNT_MAX: usize = 16; + +#[derive(Clone, PartialEq, Eq)] +pub struct WireToSurface1Pdu { + pub surface_id: u16, + pub codec_id: Codec1Type, + pub pixel_format: PixelFormat, + pub destination_rectangle: InclusiveRectangle, + pub bitmap_data: Vec, +} + +impl fmt::Debug for WireToSurface1Pdu { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WireToSurface1Pdu") + .field("surface_id", &self.surface_id) + .field("codec_id", &self.codec_id) + .field("pixel_format", &self.pixel_format) + .field("destination_rectangle", &self.destination_rectangle) + .field("bitmap_data_length", &self.bitmap_data.len()) + .finish() + } +} + +impl WireToSurface1Pdu { + const NAME: &'static str = "WireToSurface1Pdu"; + + const FIXED_PART_SIZE: usize = 2 /* SurfaceId */ + 2 /* CodecId */ + 1 /* PixelFormat */ + InclusiveRectangle::FIXED_PART_SIZE /* Dest */ + 4 /* BitmapDataLen */; +} + +impl Encode for WireToSurface1Pdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.surface_id); + dst.write_u16(self.codec_id.as_u16()); + dst.write_u8(self.pixel_format.as_u8()); + self.destination_rectangle.encode(dst)?; + dst.write_u32(cast_length!("BitmapDataLen", self.bitmap_data.len())?); + dst.write_slice(&self.bitmap_data); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.bitmap_data.len() + } +} + +impl<'a> Decode<'a> for WireToSurface1Pdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + let codec_id = + Codec1Type::from_u16(src.read_u16()).ok_or_else(|| invalid_field_err!("CodecId", "invalid codec ID"))?; + let pixel_format = PixelFormat::from_u8(src.read_u8()) + .ok_or_else(|| invalid_field_err!("PixelFormat", "invalid pixel format"))?; + let destination_rectangle = InclusiveRectangle::decode(src)?; + let bitmap_data_length = cast_length!("BitmapDataLen", src.read_u32())?; + + ensure_size!(in: src, size: bitmap_data_length); + let bitmap_data = src.read_slice(bitmap_data_length).to_vec(); + + Ok(Self { + surface_id, + codec_id, + pixel_format, + destination_rectangle, + bitmap_data, + }) + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct WireToSurface2Pdu { + pub surface_id: u16, + pub codec_id: Codec2Type, + pub codec_context_id: u32, + pub pixel_format: PixelFormat, + pub bitmap_data: Vec, +} + +impl fmt::Debug for WireToSurface2Pdu { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WireToSurface2Pdu") + .field("surface_id", &self.surface_id) + .field("codec_id", &self.codec_id) + .field("codec_context_id", &self.codec_context_id) + .field("pixel_format", &self.pixel_format) + .field("bitmap_data_length", &self.bitmap_data.len()) + .finish() + } +} + +impl WireToSurface2Pdu { + const NAME: &'static str = "WireToSurface2Pdu"; + + const FIXED_PART_SIZE: usize = 2 /* SurfaceId */ + 2 /* CodecId */ + 4 /* ContextId */ + 1 /* PixelFormat */ + 4 /* BitmapDataLen */; +} + +impl Encode for WireToSurface2Pdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.surface_id); + dst.write_u16(self.codec_id.as_u16()); + dst.write_u32(self.codec_context_id); + dst.write_u8(self.pixel_format.as_u8()); + dst.write_u32(cast_length!("BitmapDataLen", self.bitmap_data.len())?); + dst.write_slice(&self.bitmap_data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.bitmap_data.len() + } +} + +impl<'a> Decode<'a> for WireToSurface2Pdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + let codec_id = + Codec2Type::from_u16(src.read_u16()).ok_or_else(|| invalid_field_err!("CodecId", "invalid codec ID"))?; + let codec_context_id = src.read_u32(); + let pixel_format = PixelFormat::from_u8(src.read_u8()) + .ok_or_else(|| invalid_field_err!("PixelFormat", "invalid pixel format"))?; + let bitmap_data_length = cast_length!("BitmapDataLen", src.read_u32())?; + + ensure_size!(in: src, size: bitmap_data_length); + let bitmap_data = src.read_slice(bitmap_data_length).to_vec(); + + Ok(Self { + surface_id, + codec_id, + codec_context_id, + pixel_format, + bitmap_data, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeleteEncodingContextPdu { + pub surface_id: u16, + pub codec_context_id: u32, +} + +impl DeleteEncodingContextPdu { + const NAME: &'static str = "DeleteEncodingContextPdu"; + + const FIXED_PART_SIZE: usize = 2 /* SurfaceId */ + 4 /* CodecContextId */; +} + +impl Encode for DeleteEncodingContextPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.surface_id); + dst.write_u32(self.codec_context_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for DeleteEncodingContextPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + let codec_context_id = src.read_u32(); + + Ok(Self { + surface_id, + codec_context_id, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolidFillPdu { + pub surface_id: u16, + pub fill_pixel: Color, + pub rectangles: Vec, +} + +impl SolidFillPdu { + const NAME: &'static str = "CacheToSurfacePdu"; + + const FIXED_PART_SIZE: usize = 2 /* SurfaceId */ + Color::FIXED_PART_SIZE /* Color */ + 2 /* RectCount */; +} + +impl Encode for SolidFillPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.surface_id); + self.fill_pixel.encode(dst)?; + dst.write_u16(cast_length!("number of rectangles", self.rectangles.len())?); + + for rectangle in self.rectangles.iter() { + rectangle.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.rectangles.iter().map(|r| r.size()).sum::() + } +} + +impl<'a> Decode<'a> for SolidFillPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + let fill_pixel = Color::decode(src)?; + let rectangles_count = usize::from(src.read_u16()); + + ensure_size!(in: src, size: rectangles_count * InclusiveRectangle::FIXED_PART_SIZE); + let rectangles = iter::repeat_with(|| InclusiveRectangle::decode(src)) + .take(rectangles_count) + .collect::>()?; + + Ok(Self { + surface_id, + fill_pixel, + rectangles, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SurfaceToSurfacePdu { + pub source_surface_id: u16, + pub destination_surface_id: u16, + pub source_rectangle: InclusiveRectangle, + pub destination_points: Vec, +} + +impl SurfaceToSurfacePdu { + const NAME: &'static str = "SurfaceToSurfacePdu"; + + const FIXED_PART_SIZE: usize = 2 /* SourceId */ + 2 /* DestId */ + InclusiveRectangle::FIXED_PART_SIZE /* SourceRect */ + 2 /* DestPointsCount */; +} + +impl Encode for SurfaceToSurfacePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.source_surface_id); + dst.write_u16(self.destination_surface_id); + self.source_rectangle.encode(dst)?; + + dst.write_u16(cast_length!("DestinationPoints", self.destination_points.len())?); + for rectangle in self.destination_points.iter() { + rectangle.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.destination_points.iter().map(|r| r.size()).sum::() + } +} + +impl<'a> Decode<'a> for SurfaceToSurfacePdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let source_surface_id = src.read_u16(); + let destination_surface_id = src.read_u16(); + let source_rectangle = InclusiveRectangle::decode(src)?; + let destination_points_count = usize::from(src.read_u16()); + + let destination_points = iter::repeat_with(|| Point::decode(src)) + .take(destination_points_count) + .collect::>()?; + + Ok(Self { + source_surface_id, + destination_surface_id, + source_rectangle, + destination_points, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SurfaceToCachePdu { + pub surface_id: u16, + pub cache_key: u64, + pub cache_slot: u16, + pub source_rectangle: InclusiveRectangle, +} + +impl SurfaceToCachePdu { + const NAME: &'static str = "SurfaceToCachePdu"; + + const FIXED_PART_SIZE: usize = 2 /* SurfaceId */ + 8 /* CacheKey */ + 2 /* CacheSlot */ + InclusiveRectangle::FIXED_PART_SIZE /* SourceRect */; +} + +impl Encode for SurfaceToCachePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.surface_id); + dst.write_u64(self.cache_key); + dst.write_u16(self.cache_slot); + self.source_rectangle.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for SurfaceToCachePdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + let cache_key = src.read_u64(); + let cache_slot = src.read_u16(); + let source_rectangle = InclusiveRectangle::decode(src)?; + + Ok(Self { + surface_id, + cache_key, + cache_slot, + source_rectangle, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CacheToSurfacePdu { + pub cache_slot: u16, + pub surface_id: u16, + pub destination_points: Vec, +} + +impl CacheToSurfacePdu { + const NAME: &'static str = "CacheToSurfacePdu"; + + const FIXED_PART_SIZE: usize = 2 /* cache_slot */ + 2 /* surface_id */ + 2 /* npoints */; +} + +impl Encode for CacheToSurfacePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.cache_slot); + dst.write_u16(self.surface_id); + dst.write_u16(cast_length!("npoints", self.destination_points.len())?); + for point in self.destination_points.iter() { + point.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.destination_points.iter().map(|p| p.size()).sum::() + } +} + +impl<'de> Decode<'de> for CacheToSurfacePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let cache_slot = src.read_u16(); + let surface_id = src.read_u16(); + let destination_points_count = usize::from(src.read_u16()); + + let destination_points = iter::repeat_with(|| decode_cursor(src)) + .take(destination_points_count) + .collect::>()?; + + Ok(Self { + cache_slot, + surface_id, + destination_points, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CreateSurfacePdu { + pub surface_id: u16, + pub width: u16, + pub height: u16, + pub pixel_format: PixelFormat, +} + +impl CreateSurfacePdu { + const NAME: &'static str = "CreateSurfacePdu"; + + const FIXED_PART_SIZE: usize = 2 /* SurfaceId */ + 2 /* Width */ + 2 /* Height */ + 1 /* PixelFormat */; +} + +impl Encode for CreateSurfacePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.surface_id); + dst.write_u16(self.width); + dst.write_u16(self.height); + dst.write_u8(self.pixel_format.as_u8()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for CreateSurfacePdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + let width = src.read_u16(); + let height = src.read_u16(); + let pixel_format = PixelFormat::from_u8(src.read_u8()) + .ok_or_else(|| invalid_field_err!("pixelFormat", "invalid pixel format"))?; + + Ok(Self { + surface_id, + width, + height, + pixel_format, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeleteSurfacePdu { + pub surface_id: u16, +} + +impl DeleteSurfacePdu { + const NAME: &'static str = "DeleteSurfacePdu"; + + const FIXED_PART_SIZE: usize = 2 /* SurfaceId */; +} + +impl Encode for DeleteSurfacePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.surface_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for DeleteSurfacePdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + + Ok(Self { surface_id }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResetGraphicsPdu { + pub width: u32, + pub height: u32, + pub monitors: Vec, +} + +impl ResetGraphicsPdu { + const NAME: &'static str = "ResetGraphicsPdu"; + + const FIXED_PART_SIZE: usize = 4 /* Width */ + 4 /* Height */; + + fn padding_size(&self) -> usize { + RESET_GRAPHICS_PDU_SIZE - RDP_GFX_HEADER_SIZE - 12 - self.monitors.iter().map(|m| m.size()).sum::() + } +} + +impl Encode for ResetGraphicsPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.width); + dst.write_u32(self.height); + dst.write_u32(cast_length!("nMonitors", self.monitors.len())?); + + for monitor in self.monitors.iter() { + monitor.encode(dst)?; + } + + write_padding!(dst, self.padding_size()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + RESET_GRAPHICS_PDU_SIZE - RDP_GFX_HEADER_SIZE + } +} + +impl<'a> Decode<'a> for ResetGraphicsPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let width = src.read_u32(); + if width > MAX_RESET_GRAPHICS_WIDTH_HEIGHT { + return Err(invalid_field_err!("width", "invalid reset graphics width")); + } + + let height = src.read_u32(); + if height > MAX_RESET_GRAPHICS_WIDTH_HEIGHT { + return Err(invalid_field_err!("height", "invalid reset graphics height")); + } + + let monitor_count = cast_length!("monitor count", src.read_u32())?; + if monitor_count > MONITOR_COUNT_MAX { + return Err(invalid_field_err!("height", "invalid reset graphics monitor count")); + } + + let monitors = iter::repeat_with(|| Monitor::decode(src)) + .take(monitor_count) + .collect::, _>>()?; + + let pdu = Self { + width, + height, + monitors, + }; + + read_padding!(src, pdu.padding_size()); + + Ok(pdu) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MapSurfaceToOutputPdu { + pub surface_id: u16, + pub output_origin_x: u32, + pub output_origin_y: u32, +} + +impl MapSurfaceToOutputPdu { + const NAME: &'static str = "MapSurfaceToOutputPdu"; + + const FIXED_PART_SIZE: usize = 2 /* surfaceId */ + 2 /* reserved */ + 4 /* OutOriginX */ + 4 /* OutOriginY */; +} + +impl Encode for MapSurfaceToOutputPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.surface_id); + dst.write_u16(0); // reserved + dst.write_u32(self.output_origin_x); + dst.write_u32(self.output_origin_y); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for MapSurfaceToOutputPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + let _reserved = src.read_u16(); + let output_origin_x = src.read_u32(); + let output_origin_y = src.read_u32(); + + Ok(Self { + surface_id, + output_origin_x, + output_origin_y, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MapSurfaceToScaledOutputPdu { + pub surface_id: u16, + pub output_origin_x: u32, + pub output_origin_y: u32, + pub target_width: u32, + pub target_height: u32, +} + +impl MapSurfaceToScaledOutputPdu { + const NAME: &'static str = "MapSurfaceToScaledOutputPdu"; + + const FIXED_PART_SIZE: usize = 2 /* SurfaceId */ + 2 /* reserved */ + 4 /* OutOriginX */ + 4 /* OutOriginY */ + 4 /* TargetWidth */ + 4 /* TargetHeight */; +} + +impl Encode for MapSurfaceToScaledOutputPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.surface_id); + dst.write_u16(0); // reserved + dst.write_u32(self.output_origin_x); + dst.write_u32(self.output_origin_y); + dst.write_u32(self.target_width); + dst.write_u32(self.target_height); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for MapSurfaceToScaledOutputPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + let _reserved = src.read_u16(); + let output_origin_x = src.read_u32(); + let output_origin_y = src.read_u32(); + let target_width = src.read_u32(); + let target_height = src.read_u32(); + + Ok(Self { + surface_id, + output_origin_x, + output_origin_y, + target_width, + target_height, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MapSurfaceToScaledWindowPdu { + pub surface_id: u16, + pub window_id: u64, + pub mapped_width: u32, + pub mapped_height: u32, + pub target_width: u32, + pub target_height: u32, +} + +impl MapSurfaceToScaledWindowPdu { + const NAME: &'static str = "MapSurfaceToScaledWindowPdu"; + + const FIXED_PART_SIZE: usize = 2 /* SurfaceId */ + 8 /* WindowId */ + 4 /* MappedWidth */ + 4 /* MappedHeight */ + 4 /* TargetWidth */ + 4 /* TargetHeight */; +} + +impl Encode for MapSurfaceToScaledWindowPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + dst.write_u16(self.surface_id); + dst.write_u64(self.window_id); // reserved + dst.write_u32(self.mapped_width); + dst.write_u32(self.mapped_height); + dst.write_u32(self.target_width); + dst.write_u32(self.target_height); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for MapSurfaceToScaledWindowPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let surface_id = src.read_u16(); + let window_id = src.read_u64(); + let mapped_width = src.read_u32(); + let mapped_height = src.read_u32(); + let target_width = src.read_u32(); + let target_height = src.read_u32(); + + Ok(Self { + surface_id, + window_id, + mapped_width, + mapped_height, + target_width, + target_height, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EvictCacheEntryPdu { + pub cache_slot: u16, +} + +impl EvictCacheEntryPdu { + const NAME: &'static str = "EvictCacheEntryPdu"; + + const FIXED_PART_SIZE: usize = 2; +} + +impl Encode for EvictCacheEntryPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.cache_slot); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for EvictCacheEntryPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let cache_slot = src.read_u16(); + + Ok(Self { cache_slot }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StartFramePdu { + pub timestamp: Timestamp, + pub frame_id: u32, +} + +impl StartFramePdu { + const NAME: &'static str = "StartFramePdu"; + + const FIXED_PART_SIZE: usize = Timestamp::FIXED_PART_SIZE + 4 /* FrameId */; +} + +impl Encode for StartFramePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + self.timestamp.encode(dst)?; + dst.write_u32(self.frame_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for StartFramePdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let timestamp = Timestamp::decode(src)?; + let frame_id = src.read_u32(); + + Ok(Self { timestamp, frame_id }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EndFramePdu { + pub frame_id: u32, +} + +impl EndFramePdu { + const NAME: &'static str = "EndFramePdu"; + + const FIXED_PART_SIZE: usize = 4; +} + +impl Encode for EndFramePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.frame_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for EndFramePdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let frame_id = src.read_u32(); + + Ok(Self { frame_id }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CapabilitiesConfirmPdu(pub CapabilitySet); + +impl CapabilitiesConfirmPdu { + const NAME: &'static str = "CapabilitiesConfirmPdu"; +} + +impl Encode for CapabilitiesConfirmPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + self.0.encode(dst) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.0.size() + } +} + +impl<'a> Decode<'a> for CapabilitiesConfirmPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + let capability_set = CapabilitySet::decode(src)?; + + Ok(Self(capability_set)) + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum Codec1Type { + Uncompressed = 0x0, + RemoteFx = 0x3, + ClearCodec = 0x8, + Planar = 0xa, + Avc420 = 0xb, + Alpha = 0xc, + Avc444 = 0xe, + Avc444v2 = 0xf, +} + +impl Codec1Type { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum Codec2Type { + RemoteFxProgressive = 0x9, +} + +impl Codec2Type { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum PixelFormat { + XRgb = 0x20, + ARgb = 0x21, +} + +impl PixelFormat { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Timestamp { + pub milliseconds: u16, + pub seconds: u8, + pub minutes: u8, + pub hours: u16, +} + +impl Timestamp { + const NAME: &'static str = "Timestamp"; + + const FIXED_PART_SIZE: usize = 4; +} + +impl Encode for Timestamp { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let mut timestamp: u32 = 0; + + timestamp.set_bits(..10, u32::from(self.milliseconds)); + timestamp.set_bits(10..16, u32::from(self.seconds)); + timestamp.set_bits(16..22, u32::from(self.minutes)); + timestamp.set_bits(22.., u32::from(self.hours)); + + dst.write_u32(timestamp); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for Timestamp { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let timestamp = src.read_u32(); + + let milliseconds = u16::try_from(timestamp.get_bits(..10)).expect("value fits into u16"); + let seconds = u8::try_from(timestamp.get_bits(10..16)).expect("value fits into u8"); + let minutes = u8::try_from(timestamp.get_bits(16..22)).expect("value fits into u8"); + let hours = u16::try_from(timestamp.get_bits(22..)).expect("value fits into u16"); + + Ok(Self { + milliseconds, + seconds, + minutes, + hours, + }) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/mod.rs b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/mod.rs new file mode 100644 index 00000000..8b5833dd --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/mod.rs @@ -0,0 +1,309 @@ +mod graphics_messages; + +pub use graphics_messages::{ + Avc420BitmapStream, Avc444BitmapStream, CacheImportReplyPdu, CacheToSurfacePdu, CapabilitiesAdvertisePdu, + CapabilitiesConfirmPdu, CapabilitiesV103Flags, CapabilitiesV104Flags, CapabilitiesV107Flags, CapabilitiesV10Flags, + CapabilitiesV81Flags, CapabilitiesV8Flags, CapabilitySet, Codec1Type, Codec2Type, Color, CreateSurfacePdu, + DeleteEncodingContextPdu, DeleteSurfacePdu, Encoding, EndFramePdu, EvictCacheEntryPdu, FrameAcknowledgePdu, + MapSurfaceToOutputPdu, MapSurfaceToScaledOutputPdu, MapSurfaceToScaledWindowPdu, PixelFormat, Point, QuantQuality, + QueueDepth, ResetGraphicsPdu, SolidFillPdu, StartFramePdu, SurfaceToCachePdu, SurfaceToSurfacePdu, Timestamp, + WireToSurface1Pdu, WireToSurface2Pdu, +}; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ServerPdu { + WireToSurface1(WireToSurface1Pdu), + WireToSurface2(WireToSurface2Pdu), + DeleteEncodingContext(DeleteEncodingContextPdu), + SolidFill(SolidFillPdu), + SurfaceToSurface(SurfaceToSurfacePdu), + SurfaceToCache(SurfaceToCachePdu), + CacheToSurface(CacheToSurfacePdu), + EvictCacheEntry(EvictCacheEntryPdu), + CreateSurface(CreateSurfacePdu), + DeleteSurface(DeleteSurfacePdu), + StartFrame(StartFramePdu), + EndFrame(EndFramePdu), + ResetGraphics(ResetGraphicsPdu), + MapSurfaceToOutput(MapSurfaceToOutputPdu), + CapabilitiesConfirm(CapabilitiesConfirmPdu), + CacheImportReply(CacheImportReplyPdu), + MapSurfaceToScaledOutput(MapSurfaceToScaledOutputPdu), + MapSurfaceToScaledWindow(MapSurfaceToScaledWindowPdu), +} + +const RDP_GFX_HEADER_SIZE: usize = 2 /* PduType */ + 2 /* flags */ + 4 /* bufferLen */; + +impl ServerPdu { + const NAME: &'static str = "GfxServerPdu"; + + const FIXED_PART_SIZE: usize = RDP_GFX_HEADER_SIZE; +} + +impl Encode for ServerPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let buffer_length = self.size(); + + dst.write_u16(ServerPduType::from(self).as_u16()); + dst.write_u16(0); // flags + dst.write_u32(cast_length!("bufferLen", buffer_length)?); + + match self { + ServerPdu::WireToSurface1(pdu) => pdu.encode(dst), + ServerPdu::WireToSurface2(pdu) => pdu.encode(dst), + ServerPdu::DeleteEncodingContext(pdu) => pdu.encode(dst), + ServerPdu::SolidFill(pdu) => pdu.encode(dst), + ServerPdu::SurfaceToSurface(pdu) => pdu.encode(dst), + ServerPdu::SurfaceToCache(pdu) => pdu.encode(dst), + ServerPdu::CacheToSurface(pdu) => pdu.encode(dst), + ServerPdu::CreateSurface(pdu) => pdu.encode(dst), + ServerPdu::DeleteSurface(pdu) => pdu.encode(dst), + ServerPdu::ResetGraphics(pdu) => pdu.encode(dst), + ServerPdu::MapSurfaceToOutput(pdu) => pdu.encode(dst), + ServerPdu::MapSurfaceToScaledOutput(pdu) => pdu.encode(dst), + ServerPdu::MapSurfaceToScaledWindow(pdu) => pdu.encode(dst), + ServerPdu::StartFrame(pdu) => pdu.encode(dst), + ServerPdu::EndFrame(pdu) => pdu.encode(dst), + ServerPdu::EvictCacheEntry(pdu) => pdu.encode(dst), + ServerPdu::CapabilitiesConfirm(pdu) => pdu.encode(dst), + ServerPdu::CacheImportReply(pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + match self { + ServerPdu::WireToSurface1(pdu) => pdu.size(), + ServerPdu::WireToSurface2(pdu) => pdu.size(), + ServerPdu::DeleteEncodingContext(pdu) => pdu.size(), + ServerPdu::SolidFill(pdu) => pdu.size(), + ServerPdu::SurfaceToSurface(pdu) => pdu.size(), + ServerPdu::SurfaceToCache(pdu) => pdu.size(), + ServerPdu::CacheToSurface(pdu) => pdu.size(), + ServerPdu::CreateSurface(pdu) => pdu.size(), + ServerPdu::DeleteSurface(pdu) => pdu.size(), + ServerPdu::ResetGraphics(pdu) => pdu.size(), + ServerPdu::MapSurfaceToOutput(pdu) => pdu.size(), + ServerPdu::MapSurfaceToScaledOutput(pdu) => pdu.size(), + ServerPdu::MapSurfaceToScaledWindow(pdu) => pdu.size(), + ServerPdu::StartFrame(pdu) => pdu.size(), + ServerPdu::EndFrame(pdu) => pdu.size(), + ServerPdu::EvictCacheEntry(pdu) => pdu.size(), + ServerPdu::CapabilitiesConfirm(pdu) => pdu.size(), + ServerPdu::CacheImportReply(pdu) => pdu.size(), + } + } +} + +impl<'a> Decode<'a> for ServerPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let pdu_type = ServerPduType::from_u16(src.read_u16()) + .ok_or_else(|| invalid_field_err!("serverPduType", "invalid pdu type"))?; + let _flags = src.read_u16(); + let pdu_length = cast_length!("pduLen", src.read_u32())?; + + let (server_pdu, buffer_length) = { + let pdu = match pdu_type { + ServerPduType::DeleteEncodingContext => { + ServerPdu::DeleteEncodingContext(DeleteEncodingContextPdu::decode(src)?) + } + ServerPduType::WireToSurface1 => ServerPdu::WireToSurface1(WireToSurface1Pdu::decode(src)?), + ServerPduType::WireToSurface2 => ServerPdu::WireToSurface2(WireToSurface2Pdu::decode(src)?), + ServerPduType::SolidFill => ServerPdu::SolidFill(SolidFillPdu::decode(src)?), + ServerPduType::SurfaceToSurface => ServerPdu::SurfaceToSurface(SurfaceToSurfacePdu::decode(src)?), + ServerPduType::SurfaceToCache => ServerPdu::SurfaceToCache(SurfaceToCachePdu::decode(src)?), + ServerPduType::CacheToSurface => ServerPdu::CacheToSurface(CacheToSurfacePdu::decode(src)?), + ServerPduType::EvictCacheEntry => ServerPdu::EvictCacheEntry(EvictCacheEntryPdu::decode(src)?), + ServerPduType::CreateSurface => ServerPdu::CreateSurface(CreateSurfacePdu::decode(src)?), + ServerPduType::DeleteSurface => ServerPdu::DeleteSurface(DeleteSurfacePdu::decode(src)?), + ServerPduType::StartFrame => ServerPdu::StartFrame(StartFramePdu::decode(src)?), + ServerPduType::EndFrame => ServerPdu::EndFrame(EndFramePdu::decode(src)?), + ServerPduType::ResetGraphics => ServerPdu::ResetGraphics(ResetGraphicsPdu::decode(src)?), + ServerPduType::MapSurfaceToOutput => ServerPdu::MapSurfaceToOutput(MapSurfaceToOutputPdu::decode(src)?), + ServerPduType::CapabilitiesConfirm => { + ServerPdu::CapabilitiesConfirm(CapabilitiesConfirmPdu::decode(src)?) + } + ServerPduType::CacheImportReply => ServerPdu::CacheImportReply(CacheImportReplyPdu::decode(src)?), + ServerPduType::MapSurfaceToScaledOutput => { + ServerPdu::MapSurfaceToScaledOutput(MapSurfaceToScaledOutputPdu::decode(src)?) + } + ServerPduType::MapSurfaceToScaledWindow => { + ServerPdu::MapSurfaceToScaledWindow(MapSurfaceToScaledWindowPdu::decode(src)?) + } + _ => return Err(invalid_field_err!("pduType", "invalid pdu type")), + }; + let buffer_length = pdu.size(); + + (pdu, buffer_length) + }; + + if buffer_length != pdu_length { + Err(invalid_field_err!("len", "invalid pdu length")) + } else { + Ok(server_pdu) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ClientPdu { + FrameAcknowledge(FrameAcknowledgePdu), + CapabilitiesAdvertise(CapabilitiesAdvertisePdu), +} + +impl ClientPdu { + const NAME: &'static str = "GfxClientPdu"; + + const FIXED_PART_SIZE: usize = RDP_GFX_HEADER_SIZE; +} + +impl Encode for ClientPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(ClientPduType::from(self).as_u16()); + dst.write_u16(0); // flags + dst.write_u32(cast_length!("bufferLen", self.size())?); + + match self { + ClientPdu::FrameAcknowledge(pdu) => pdu.encode(dst), + ClientPdu::CapabilitiesAdvertise(pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + + match self { + ClientPdu::FrameAcknowledge(pdu) => pdu.size(), + ClientPdu::CapabilitiesAdvertise(pdu) => pdu.size(), + } + } +} + +impl<'a> Decode<'a> for ClientPdu { + fn decode(src: &mut ReadCursor<'a>) -> DecodeResult { + let pdu_type = ClientPduType::from_u16(src.read_u16()) + .ok_or_else(|| invalid_field_err!("clientPduType", "invalid pdu type"))?; + let _flags = src.read_u16(); + let pdu_length = cast_length!("bufferLen", src.read_u32())?; + + let client_pdu = match pdu_type { + ClientPduType::FrameAcknowledge => ClientPdu::FrameAcknowledge(FrameAcknowledgePdu::decode(src)?), + ClientPduType::CapabilitiesAdvertise => { + ClientPdu::CapabilitiesAdvertise(CapabilitiesAdvertisePdu::decode(src)?) + } + _ => return Err(invalid_field_err!("pduType", "invalid pdu type")), + }; + + if client_pdu.size() != pdu_length { + Err(invalid_field_err!("len", "invalid pdu length")) + } else { + Ok(client_pdu) + } + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum ClientPduType { + FrameAcknowledge = 0x0d, + CacheImportOffer = 0x10, + CapabilitiesAdvertise = 0x12, + QoeFrameAcknowledge = 0x16, +} + +impl ClientPduType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +impl<'a> From<&'a ClientPdu> for ClientPduType { + fn from(c: &'a ClientPdu) -> Self { + match c { + ClientPdu::FrameAcknowledge(_) => Self::FrameAcknowledge, + ClientPdu::CapabilitiesAdvertise(_) => Self::CapabilitiesAdvertise, + } + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum ServerPduType { + WireToSurface1 = 0x01, + WireToSurface2 = 0x02, + DeleteEncodingContext = 0x03, + SolidFill = 0x04, + SurfaceToSurface = 0x05, + SurfaceToCache = 0x06, + CacheToSurface = 0x07, + EvictCacheEntry = 0x08, + CreateSurface = 0x09, + DeleteSurface = 0x0a, + StartFrame = 0x0b, + EndFrame = 0x0c, + ResetGraphics = 0x0e, + MapSurfaceToOutput = 0x0f, + CacheImportReply = 0x11, + CapabilitiesConfirm = 0x13, + MapSurfaceToWindow = 0x15, + MapSurfaceToScaledOutput = 0x17, + MapSurfaceToScaledWindow = 0x18, +} + +impl ServerPduType { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +impl<'a> From<&'a ServerPdu> for ServerPduType { + fn from(s: &'a ServerPdu) -> Self { + match s { + ServerPdu::WireToSurface1(_) => Self::WireToSurface1, + ServerPdu::WireToSurface2(_) => Self::WireToSurface2, + ServerPdu::DeleteEncodingContext(_) => Self::DeleteEncodingContext, + ServerPdu::SolidFill(_) => Self::SolidFill, + ServerPdu::SurfaceToSurface(_) => Self::SurfaceToSurface, + ServerPdu::SurfaceToCache(_) => Self::SurfaceToCache, + ServerPdu::CacheToSurface(_) => Self::CacheToSurface, + ServerPdu::EvictCacheEntry(_) => Self::EvictCacheEntry, + ServerPdu::CreateSurface(_) => Self::CreateSurface, + ServerPdu::DeleteSurface(_) => Self::DeleteSurface, + ServerPdu::StartFrame(_) => Self::StartFrame, + ServerPdu::EndFrame(_) => Self::EndFrame, + ServerPdu::ResetGraphics(_) => Self::ResetGraphics, + ServerPdu::MapSurfaceToOutput(_) => Self::MapSurfaceToOutput, + ServerPdu::MapSurfaceToScaledOutput(_) => Self::MapSurfaceToScaledOutput, + ServerPdu::MapSurfaceToScaledWindow(_) => Self::MapSurfaceToScaledWindow, + ServerPdu::CapabilitiesConfirm(_) => Self::CapabilitiesConfirm, + ServerPdu::CacheImportReply(_) => Self::CacheImportReply, + } + } +} diff --git a/crates/ironrdp-pdu/src/rdp/vc/dvc/mod.rs b/crates/ironrdp-pdu/src/rdp/vc/dvc/mod.rs new file mode 100644 index 00000000..4fcc11bb --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/vc/dvc/mod.rs @@ -0,0 +1 @@ +pub mod gfx; diff --git a/crates/ironrdp-pdu/src/rdp/vc/mod.rs b/crates/ironrdp-pdu/src/rdp/vc/mod.rs new file mode 100644 index 00000000..7d166540 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/vc/mod.rs @@ -0,0 +1,116 @@ +pub mod dvc; + +#[cfg(test)] +mod tests; + +use std::{io, str}; + +use bitflags::bitflags; +use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; +use thiserror::Error; + +use crate::PduError; + +const CHANNEL_PDU_HEADER_SIZE: usize = 8; + +/// Channel PDU Header (CHANNEL_PDU_HEADER) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChannelPduHeader { + /// The total length in bytes of the uncompressed channel data, excluding this header + /// + /// The data can span multiple Virtual Channel PDUs and the individual chunks will need to be + /// reassembled in that case (section 3.1.5.2.2 of MS-RDPBCGR). + pub length: u32, + pub flags: ChannelControlFlags, +} + +impl ChannelPduHeader { + const NAME: &'static str = "ChannelPduHeader"; + + const FIXED_PART_SIZE: usize = CHANNEL_PDU_HEADER_SIZE; +} + +impl Encode for ChannelPduHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.length); + dst.write_u32(self.flags.bits()); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for ChannelPduHeader { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let total_length = src.read_u32(); + let flags = ChannelControlFlags::from_bits_truncate(src.read_u32()); + Ok(Self { + length: total_length, + flags, + }) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ChannelControlFlags: u32 { + const FLAG_FIRST = 0x0000_0001; + const FLAG_LAST = 0x0000_0002; + const FLAG_SHOW_PROTOCOL = 0x0000_0010; + const FLAG_SUSPEND = 0x0000_0020; + const FLAG_RESUME = 0x0000_0040; + const FLAG_SHADOW_PERSISTENT = 0x0000_0080; + const PACKET_COMPRESSED = 0x0020_0000; + const PACKET_AT_FRONT = 0x0040_0000; + const PACKET_FLUSHED = 0x0080_0000; + const COMPRESSION_TYPE_MASK = 0x000F_0000; + } +} + +#[derive(Debug, Error)] +pub enum ChannelError { + #[error("IO error")] + IOError(#[from] io::Error), + #[error("from UTF-8 error")] + FromUtf8Error(#[from] std::string::FromUtf8Error), + #[error("invalid channel PDU header")] + InvalidChannelPduHeader, + #[error("invalid channel total data length")] + InvalidChannelTotalDataLength, + #[error("invalid DVC PDU type")] + InvalidDvcPduType, + #[error("invalid DVC id length value")] + InvalidDVChannelIdLength, + #[error("invalid DVC data length value")] + InvalidDvcDataLength, + #[error("invalid DVC capabilities version")] + InvalidDvcCapabilitiesVersion, + #[error("invalid DVC message size")] + InvalidDvcMessageSize, + #[error("invalid DVC total message size: actual ({actual}) > expected ({expected})")] + InvalidDvcTotalMessageSize { actual: usize, expected: usize }, + #[error("PDU error: {0}")] + Pdu(PduError), +} + +impl From for ChannelError { + fn from(e: PduError) -> Self { + Self::Pdu(e) + } +} + +impl From for io::Error { + fn from(e: ChannelError) -> io::Error { + io::Error::other(format!("Virtual channel error: {e}")) + } +} diff --git a/crates/ironrdp-pdu/src/rdp/vc/tests.rs b/crates/ironrdp-pdu/src/rdp/vc/tests.rs new file mode 100644 index 00000000..ef31a096 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/vc/tests.rs @@ -0,0 +1,40 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode, encode_vec}; + +use super::*; + +const CHANNEL_CHUNK_LENGTH_DEFAULT: u32 = 1600; +const CHANNEL_PDU_HEADER_BUFFER: [u8; CHANNEL_PDU_HEADER_SIZE] = [0x40, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]; + +static CHANNEL_PDU_HEADER: LazyLock = LazyLock::new(|| ChannelPduHeader { + length: CHANNEL_CHUNK_LENGTH_DEFAULT, + flags: ChannelControlFlags::FLAG_FIRST, +}); + +#[test] +fn from_buffer_correct_parses_channel_header() { + assert_eq!( + CHANNEL_PDU_HEADER.clone(), + decode(CHANNEL_PDU_HEADER_BUFFER.as_ref()).unwrap(), + ); +} + +#[test] +fn to_buffer_correct_serializes_channel_header() { + let channel_header = CHANNEL_PDU_HEADER.clone(); + + let buffer = encode_vec(&channel_header).unwrap(); + + assert_eq!(CHANNEL_PDU_HEADER_BUFFER.as_ref(), buffer.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_channel_header() { + let channel_header = CHANNEL_PDU_HEADER.clone(); + let expected_buf_len = CHANNEL_PDU_HEADER_BUFFER.len(); + + let len = channel_header.size(); + + assert_eq!(expected_buf_len, len); +} diff --git a/crates/ironrdp-pdu/src/tpdu.rs b/crates/ironrdp-pdu/src/tpdu.rs new file mode 100644 index 00000000..f008ef10 --- /dev/null +++ b/crates/ironrdp-pdu/src/tpdu.rs @@ -0,0 +1,186 @@ +use ironrdp_core::{ + ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, unexpected_message_type_err, ReadCursor, + WriteCursor, +}; + +use crate::tpkt::TpktHeader; +use crate::{DecodeResult, EncodeResult}; + +/// TPDU type used during X.224 messages exchange +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct TpduCode(u8); + +impl TpduCode { + pub const CONNECTION_REQUEST: Self = Self(0xE0); + pub const CONNECTION_CONFIRM: Self = Self(0xD0); + pub const DISCONNECT_REQUEST: Self = Self(0x80); + pub const DATA: Self = Self(0xF0); + pub const ERROR: Self = Self(0x70); + + pub fn header_fixed_part_size(self) -> usize { + if self == TpduCode::DATA { + TpduHeader::DATA_FIXED_PART_SIZE + } else { + TpduHeader::NOT_DATA_FIXED_PART_SIZE + } + } + + pub fn check_expected(self, expected: TpduCode) -> DecodeResult<()> { + if self == expected { + Ok(()) + } else { + Err(unexpected_message_type_err!(TpduHeader::NAME, self.0)) + } + } +} + +impl From for TpduCode { + fn from(value: u8) -> Self { + Self(value) + } +} + +impl From for u8 { + fn from(value: TpduCode) -> Self { + value.0 + } +} + +/// TPDU header, follows a TPKT header +/// +/// TPDUs are defined in: +/// +/// - — X.224: Information technology - Open Systems +/// Interconnection - Protocol for providing the connection-mode transport service +/// - RDP uses only TPDUs of class 0, the "simple class" defined in section 8 of X.224 +/// +/// ```diagram +/// TPDU Header +/// ____________________ byte +/// | | +/// | LI | 1 +/// |____________________| +/// | | +/// | Code | 2 +/// |____________________| +/// | | +/// | | 3 +/// |_______DST-REF______| +/// | | +/// | | 4 +/// |____________________| +/// | | +/// | | 5 +/// |_______SRC-REF______| +/// | | +/// | | 6 +/// |____________________| +/// | | +/// | Class | 7 +/// |____________________| +/// | ... | +///``` +#[derive(Debug, PartialEq, Eq)] +pub struct TpduHeader { + /// Length indicator field as defined in section 13.2.1 of X.224. + /// + /// The length indicated by LI shall be the header length in octets including + /// parameters, but excluding the length indicator field and user data, if any. + /// + /// ```diagram + /// ——————————————————————————————————————————————— + /// | LI | Fixed part | Variable part | User data | + /// |————|————————————|———————————————|———————————| + /// | | <—————————— LI ——————————> | | + /// | <—————————— Header ———————————> | | + /// ``` + pub li: u8, + /// TPDU code, used to define the structure of the remaining header. + pub code: TpduCode, +} + +impl TpduHeader { + pub const CONNECTION_REQUEST_FIXED_PART_SIZE: usize = Self::NOT_DATA_FIXED_PART_SIZE; + + pub const CONNECTION_CONFIRM_FIXED_PART_SIZE: usize = Self::NOT_DATA_FIXED_PART_SIZE; + + pub const DISCONNECT_REQUEST_FIXED_PART_SIZE: usize = Self::NOT_DATA_FIXED_PART_SIZE; + + pub const DATA_FIXED_PART_SIZE: usize = 3; + + pub const ERROR_FIXED_PART_SIZE: usize = Self::NOT_DATA_FIXED_PART_SIZE; + + pub const NOT_DATA_FIXED_PART_SIZE: usize = 7; + + pub const NAME: &'static str = "TpduHeader"; + + const FIXED_PART_SIZE: usize = Self::DATA_FIXED_PART_SIZE; + + pub fn read(src: &mut ReadCursor<'_>, tpkt: &TpktHeader) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let li = src.read_u8(); // LI + let code = TpduCode::from(src.read_u8()); // Code + + if usize::from(li) + 1 + TpktHeader::SIZE > usize::from(tpkt.packet_length) { + return Err(invalid_field_err( + Self::NAME, + "li", + "tpdu length greater than tpkt length", + )); + } + + // The value 255 (1111 1111) is reserved for possible extensions. + if li == 0b1111_1111 { + return Err(invalid_field_err( + Self::NAME, + "li", + "unsupported X.224 extension (suggested by LI field set to 255)", + )); + } + + if code == TpduCode::DATA { + read_padding!(src, 1); // EOT + } else { + ensure_size!(in: src, size: 5); + read_padding!(src, 5); // DST-REF, SRC-REF, Class 0 + } + + Ok(Self { li, code }) + } + + pub fn write(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + const EOT_BYTE: u8 = 0x80; + + ensure_fixed_part_size!(in: dst); + + dst.write_u8(self.li); // LI + dst.write_u8(u8::from(self.code)); // Code + + if self.code == TpduCode::DATA { + dst.write_u8(EOT_BYTE); // EOT + } else { + ensure_size!(in: dst, size: 5); + dst.write_u16(0); // DST-REF + dst.write_u16(0); // SRC-REF + dst.write_u8(0); // Class 0 + } + + Ok(()) + } + + /// Fixed part of the TPDU header. + pub fn fixed_part_size(&self) -> usize { + self.code.header_fixed_part_size() + } + + /// Variable part of the TPDU header. + pub fn variable_part_size(&self) -> usize { + self.size() - self.fixed_part_size() + } + + /// Size of the whole TPDU header, including LI field and variable part. + pub fn size(&self) -> usize { + usize::from(self.li) + 1 + } +} diff --git a/crates/ironrdp-pdu/src/tpkt.rs b/crates/ironrdp-pdu/src/tpkt.rs new file mode 100644 index 00000000..95a96f47 --- /dev/null +++ b/crates/ironrdp-pdu/src/tpkt.rs @@ -0,0 +1,84 @@ +use ironrdp_core::{ + ensure_fixed_part_size, read_padding, unsupported_version_err, write_padding, ReadCursor, WriteCursor, +}; + +use crate::{DecodeResult, EncodeResult}; + +/// TPKT header +/// +/// TPKTs are defined in: +/// +/// - — RFC 1006 - ISO Transport Service on top of the TCP +/// - — ITU-T T.123 (01/2007) - Network-specific data protocol +/// stacks for multimedia conferencing +/// +/// ```diagram +/// TPKT Header +/// ____________________ byte +/// | | +/// | 3 (version) | 1 +/// |____________________| +/// | | +/// | Reserved | 2 +/// |____________________| +/// | | +/// | Length (MSB) | 3 +/// |____________________| +/// | | +/// | Length (LSB) | 4 +/// |____________________| +/// | | +/// | X.224 TPDU | 5 - ? +/// .... +/// ``` +/// +/// A TPKT header is of fixed length 4, and the following X.224 TPDU is at least three bytes long. +/// Therefore, the minimum TPKT length is 7, and the maximum TPKT length is 65535. Because the TPKT +/// length includes the TPKT header (4 bytes), the maximum X.224 TPDU length is 65531. +#[derive(PartialEq, Eq, Debug)] +pub struct TpktHeader { + /// This field contains the length of entire packet in octets, including packet-header. + pub packet_length: u16, +} + +impl TpktHeader { + pub const VERSION: u8 = 3; + + pub const SIZE: usize = 4; + + pub const NAME: &'static str = "TpktHeader"; + + const FIXED_PART_SIZE: usize = Self::SIZE; + + pub fn read(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let version = src.read_u8(); + + if version != Self::VERSION { + return Err(unsupported_version_err!("TPKT version", version)); + } + + read_padding!(src, 1); + + let packet_length = src.read_u16_be(); + + Ok(Self { packet_length }) + } + + pub fn write(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u8(Self::VERSION); + + write_padding!(dst, 1); + + dst.write_u16_be(self.packet_length); + + Ok(()) + } + + pub fn packet_length(&self) -> usize { + usize::from(self.packet_length) + } +} diff --git a/crates/ironrdp-pdu/src/utf16.rs b/crates/ironrdp-pdu/src/utf16.rs new file mode 100644 index 00000000..41d21109 --- /dev/null +++ b/crates/ironrdp-pdu/src/utf16.rs @@ -0,0 +1,26 @@ +use std::string::FromUtf16Error; + +pub fn read_utf16_string(utf16_payload: &[u8], utf16_size_hint: Option) -> Result { + let mut trimmed_utf16: Vec = if let Some(size_hint) = utf16_size_hint { + Vec::with_capacity(size_hint) + } else { + Vec::with_capacity(utf16_payload.len() / 2) + }; + + for chunk in utf16_payload.chunks_exact(2) { + let code_unit = u16::from_le_bytes([chunk[0], chunk[1]]); + + // Stop reading at the null terminator + if code_unit == 0 { + break; + } + + trimmed_utf16.push(code_unit); + } + + String::from_utf16(&trimmed_utf16) +} + +pub fn null_terminated_utf16_encoded_len(utf8: &str) -> usize { + utf8.encode_utf16().count() * 2 + 2 +} diff --git a/crates/ironrdp-pdu/src/utils.rs b/crates/ironrdp-pdu/src/utils.rs new file mode 100644 index 00000000..edfa115d --- /dev/null +++ b/crates/ironrdp-pdu/src/utils.rs @@ -0,0 +1,315 @@ +use core::fmt::Debug; +use core::ops::Add; + +use byteorder::{LittleEndian, ReadBytesExt as _}; +use ironrdp_core::{ensure_size, invalid_field_err, other_err, ReadCursor, WriteCursor}; +use num_derive::FromPrimitive; + +use crate::{DecodeResult, EncodeResult}; + +pub fn split_u64(value: u64) -> (u32, u32) { + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")] + let low = + u32::try_from(value & 0xFFFF_FFFF).expect("masking with 0xFFFF_FFFF ensures that the value fits into u32"); + + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")] + let high = u32::try_from(value >> 32).expect("(u64 >> 32) fits into u32"); + + (low, high) +} + +pub fn combine_u64(lo: u32, hi: u32) -> u64 { + let mut position_bytes = [0u8; size_of::()]; + position_bytes[..size_of::()].copy_from_slice(&lo.to_le_bytes()); + position_bytes[size_of::()..].copy_from_slice(&hi.to_le_bytes()); + u64::from_le_bytes(position_bytes) +} + +pub fn to_utf16_bytes(value: &str) -> Vec { + value + .encode_utf16() + .flat_map(|i| i.to_le_bytes().to_vec()) + .collect::>() +} + +pub fn from_utf16_bytes(mut value: &[u8]) -> String { + let mut value_u16 = vec![0x00; value.len() / 2]; + + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (prior constrain)")] + value + .read_u16_into::(value_u16.as_mut()) + .expect("read_u16_into cannot fail at this point"); + + String::from_utf16_lossy(value_u16.as_ref()) +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +pub enum CharacterSet { + Ansi = 1, + Unicode = 2, +} + +impl CharacterSet { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub fn as_u16(self) -> u16 { + self as u16 + } +} + +// Read a string from the cursor, using the specified character set. +// +// If read_null_terminator is true, the string will be read until a null terminator is found. +// Otherwise, the string will be read until the end of the cursor. If the next character is a null +// terminator, an empty string will be returned (without consuming the null terminator). +pub fn read_string_from_cursor( + cursor: &mut ReadCursor<'_>, + character_set: CharacterSet, + read_null_terminator: bool, +) -> DecodeResult { + let size = if character_set == CharacterSet::Unicode { + let code_units = if read_null_terminator { + // Find null or read all if null is not found + cursor + .remaining() + .chunks_exact(2) + .position(|chunk| chunk == [0, 0]) + .map(|null_terminator_pos| null_terminator_pos + 1) // Read null code point + .unwrap_or(cursor.len() / 2) + } else { + // UTF16 uses 2 bytes per code unit, so we need to read an even number of bytes + cursor.len() / 2 + }; + + code_units * 2 + } else if read_null_terminator { + // Find null or read all if null is not found + cursor + .remaining() + .iter() + .position(|&i| i == 0) + .map(|null_terminator_pos| null_terminator_pos + 1) // Read null code point + .unwrap_or(cursor.len()) + } else { + // Read all + cursor.len() + }; + + // Empty string, nothing to do + if size == 0 { + return Ok(String::new()); + } + + let result = match character_set { + CharacterSet::Unicode => { + ensure_size!(ctx: "Decode string (UTF-16)", in: cursor, size: size); + let mut slice = cursor.read_slice(size); + + let str_buffer = &mut slice; + let mut u16_buffer = vec![0u16; str_buffer.len() / 2]; + + #[expect(clippy::missing_panics_doc, reason = "unreachable panic (prior constrain)")] + str_buffer + .read_u16_into::(u16_buffer.as_mut()) + .expect("BUG: str_buffer is always even for UTF16"); + + String::from_utf16(&u16_buffer) + .map_err(|_| invalid_field_err!("UTF16 decode", "buffer", "Failed to decode UTF16 string"))? + } + CharacterSet::Ansi => { + ensure_size!(ctx: "Decode string (UTF-8)", in: cursor, size: size); + let slice = cursor.read_slice(size); + String::from_utf8(slice.to_vec()) + .map_err(|_| invalid_field_err!("UTF8 decode", "buffer", "Failed to decode UTF8 string"))? + } + }; + + Ok(result.trim_end_matches('\0').into()) +} + +pub fn decode_string(src: &[u8], character_set: CharacterSet, read_null_terminator: bool) -> DecodeResult { + read_string_from_cursor(&mut ReadCursor::new(src), character_set, read_null_terminator) +} + +pub fn read_multistring_from_cursor( + cursor: &mut ReadCursor<'_>, + character_set: CharacterSet, +) -> DecodeResult> { + let mut strings = Vec::new(); + + loop { + let string = read_string_from_cursor(cursor, character_set, true)?; + if string.is_empty() { + // empty string indicates the end of the multi-string array + // (we hit two null terminators in a row) + break; + } + + strings.push(string); + } + + Ok(strings) +} + +pub fn encode_string( + dst: &mut [u8], + value: &str, + character_set: CharacterSet, + write_null_terminator: bool, +) -> EncodeResult { + let (buffer, ctx) = match character_set { + CharacterSet::Unicode => { + let mut buffer = to_utf16_bytes(value); + if write_null_terminator { + buffer.extend_from_slice(&[0, 0]); + } + (buffer, "Encode string (UTF-16)") + } + CharacterSet::Ansi => { + let mut buffer = value.as_bytes().to_vec(); + if write_null_terminator { + buffer.push(0); + } + (buffer, "Encode string (UTF-8)") + } + }; + + let len = buffer.len(); + + ensure_size!(ctx: ctx, in: dst, size: len); + dst[..len].copy_from_slice(&buffer); + + Ok(len) +} + +pub fn write_string_to_cursor( + cursor: &mut WriteCursor<'_>, + value: &str, + character_set: CharacterSet, + write_null_terminator: bool, +) -> EncodeResult<()> { + let len = encode_string(cursor.remaining_mut(), value, character_set, write_null_terminator)?; + cursor.advance(len); + Ok(()) +} + +pub fn write_multistring_to_cursor( + cursor: &mut WriteCursor<'_>, + strings: &[String], + character_set: CharacterSet, +) -> EncodeResult<()> { + // Write each string to cursor, separated by a null terminator + for string in strings { + write_string_to_cursor(cursor, string, character_set, true)?; + } + + // Write final null terminator signifying the end of the multi-string + match character_set { + CharacterSet::Unicode => { + ensure_size!(ctx: "Encode multistring (UTF-16)", in: cursor, size: 2); + cursor.write_u16(0) + } + CharacterSet::Ansi => { + ensure_size!(ctx: "Encode multistring (UTF-8)", in: cursor, size: 1); + cursor.write_u8(0) + } + } + + Ok(()) +} + +/// Returns the length in bytes of the encoded value +/// based on the passed CharacterSet and with_null_terminator flag. +pub fn encoded_str_len(value: &str, character_set: CharacterSet, with_null_terminator: bool) -> usize { + match character_set { + CharacterSet::Ansi => value.len() + if with_null_terminator { 1 } else { 0 }, + CharacterSet::Unicode => value.encode_utf16().count() * 2 + if with_null_terminator { 2 } else { 0 }, + } +} + +/// Returns the length in bytes of the encoded multi-string +/// based on the passed CharacterSet. +pub fn encoded_multistring_len(strings: &[String], character_set: CharacterSet) -> usize { + strings + .iter() + .map(|s| encoded_str_len(s, character_set, true)) + .sum::() + + if character_set == CharacterSet::Unicode { 2 } else { 1 } +} + +// FIXME: legacy trait +pub trait SplitTo { + #[must_use] + fn split_to(&mut self, n: usize) -> Self; +} + +impl SplitTo for &[T] { + fn split_to(&mut self, n: usize) -> Self { + assert!(n <= self.len()); + + let (a, b) = self.split_at(n); + *self = b; + + a + } +} + +impl SplitTo for &mut [T] { + fn split_to(&mut self, n: usize) -> Self { + assert!(n <= self.len()); + + let (a, b) = core::mem::take(self).split_at_mut(n); + *self = b; + + a + } +} + +pub trait CheckedAdd: Sized + Add { + fn checked_add(self, rhs: Self) -> Option; +} + +// Implement the trait for usize and u32 +impl CheckedAdd for usize { + fn checked_add(self, rhs: Self) -> Option { + usize::checked_add(self, rhs) + } +} + +impl CheckedAdd for u32 { + fn checked_add(self, rhs: Self) -> Option { + u32::checked_add(self, rhs) + } +} + +// Utility function for checked addition that returns a PduResult +pub fn checked_sum(values: &[T]) -> DecodeResult +where + T: CheckedAdd + Copy + Debug, +{ + values.split_first().map_or_else( + || Err(other_err!("empty array provided to checked_sum")), + |(&first, rest)| { + rest.iter().try_fold(first, |acc, &val| { + acc.checked_add(val) + .ok_or_else(|| other_err!("overflow detected during addition")) + }) + }, + ) +} + +/// Utility function that panics on overflow +/// +/// # Panics +/// +/// Panics if sum of values overflows. +// FIXME: Is it really something we want to expose from ironrdp-pdu? +pub fn strict_sum(values: &[T]) -> T +where + T: CheckedAdd + Copy + Debug, +{ + checked_sum::(values).expect("overflow detected during addition") +} diff --git a/crates/ironrdp-pdu/src/x224.rs b/crates/ironrdp-pdu/src/x224.rs new file mode 100644 index 00000000..a6da76d4 --- /dev/null +++ b/crates/ironrdp-pdu/src/x224.rs @@ -0,0 +1,146 @@ +use std::borrow::Cow; + +use ironrdp_core::{ + cast_length, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, + WriteCursor, +}; + +use crate::tpdu::{TpduCode, TpduHeader}; +use crate::tpkt::TpktHeader; +use crate::{impl_x224_pdu_borrowing, Pdu}; + +pub trait X224Pdu<'de>: Sized { + const X224_NAME: &'static str; + + const TPDU_CODE: TpduCode; + + fn x224_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()>; + + fn x224_body_decode(src: &mut ReadCursor<'de>, tpkt: &TpktHeader, tpdu: &TpduHeader) -> DecodeResult; + + fn tpdu_header_variable_part_size(&self) -> usize; + + fn tpdu_user_data_size(&self) -> usize; +} + +impl<'de, T> Pdu for T +where + T: X224Pdu<'de>, +{ + const NAME: &'static str = T::X224_NAME; +} + +#[derive(Debug, Eq, PartialEq)] +pub struct X224(pub T); + +impl<'de, T> Encode for X224 +where + T: X224Pdu<'de>, +{ + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let packet_length = self.size(); + + ensure_size!(in: dst, size: packet_length); + + TpktHeader { + packet_length: cast_length!("packet length", packet_length)?, + } + .write(dst)?; + + let li = cast_length!( + "length indicator", + (T::TPDU_CODE.header_fixed_part_size() + self.0.tpdu_header_variable_part_size() - 1) + )?; + + TpduHeader { li, code: T::TPDU_CODE }.write(dst)?; + + self.0.x224_body_encode(dst) + } + + fn name(&self) -> &'static str { + T::X224_NAME + } + + fn size(&self) -> usize { + TpktHeader::SIZE + + T::TPDU_CODE.header_fixed_part_size() + + self.0.tpdu_header_variable_part_size() + + self.0.tpdu_user_data_size() + } +} + +impl<'de, T> Decode<'de> for X224 +where + T: X224Pdu<'de>, +{ + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let tpkt = TpktHeader::read(src)?; + + ensure_size!(in: src, size: tpkt.packet_length().saturating_sub(TpktHeader::SIZE)); + + let tpdu = TpduHeader::read(src, &tpkt)?; + tpdu.code.check_expected(T::TPDU_CODE)?; + + if tpdu.size() < tpdu.fixed_part_size() { + return Err(invalid_field_err( + "TpduHeader", + "li", + "fixed part bigger than total header size", + )); + } + + T::x224_body_decode(src, &tpkt, &tpdu).map(X224) + } +} + +pub struct X224Data<'a> { + pub data: Cow<'a, [u8]>, +} + +impl_x224_pdu_borrowing!(X224Data<'_>, OwnedX224Data); + +impl IntoOwned for X224Data<'_> { + type Owned = OwnedX224Data; + + fn into_owned(self) -> Self::Owned { + X224Data { + data: Cow::Owned(self.data.into_owned()), + } + } +} + +impl<'de> X224Pdu<'de> for X224Data<'de> { + const X224_NAME: &'static str = "X.224 Data"; + + const TPDU_CODE: TpduCode = TpduCode::DATA; + + fn x224_body_encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.data.len()); + dst.write_slice(&self.data); + + Ok(()) + } + + fn x224_body_decode(src: &mut ReadCursor<'de>, tpkt: &TpktHeader, tpdu: &TpduHeader) -> DecodeResult { + let user_data_size = user_data_size(tpkt, tpdu); + + ensure_size!(in: src, size: user_data_size); + let data = src.read_slice(user_data_size); + + Ok(Self { + data: Cow::Borrowed(data), + }) + } + + fn tpdu_header_variable_part_size(&self) -> usize { + 0 + } + + fn tpdu_user_data_size(&self) -> usize { + self.data.len() + } +} + +pub fn user_data_size(tpkt: &TpktHeader, tpdu: &TpduHeader) -> usize { + tpkt.packet_length() - TpktHeader::SIZE - tpdu.size() +} diff --git a/crates/ironrdp-propertyset/Cargo.toml b/crates/ironrdp-propertyset/Cargo.toml new file mode 100644 index 00000000..4cb05bb6 --- /dev/null +++ b/crates/ironrdp-propertyset/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ironrdp-propertyset" +version = "0.1.0" +readme = "README.md" +description = "A key-value store for configuration options" +publish = false # TODO: publish +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +tracing = { version = "0.1", features = ["log"] } + +[lints] +workspace = true diff --git a/crates/ironrdp-propertyset/README.md b/crates/ironrdp-propertyset/README.md new file mode 100644 index 00000000..4587bba6 --- /dev/null +++ b/crates/ironrdp-propertyset/README.md @@ -0,0 +1,7 @@ +# IronRDP PropertySet + +The main type is `PropertySet`, a key-value store for configuration options. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-propertyset/src/lib.rs b/crates/ironrdp-propertyset/src/lib.rs new file mode 100644 index 00000000..faa60a53 --- /dev/null +++ b/crates/ironrdp-propertyset/src/lib.rs @@ -0,0 +1,169 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![no_std] + +extern crate alloc; + +use alloc::borrow::Cow; +use alloc::collections::BTreeMap; +use alloc::string::String; +use core::fmt::{self, Display}; + +use tracing::debug; + +pub type Key = Cow<'static, str>; + +/// Key-value store for configuration keys. +#[derive(Clone, Default, PartialEq, Eq)] +pub struct PropertySet { + inner: BTreeMap, +} + +impl PropertySet { + pub fn new() -> Self { + Self::default() + } + + pub fn insert(&mut self, key: impl Into, value: impl Into) -> Option { + let (key, value) = (key.into(), value.into()); + debug!("PropertySet::insert({key}, {value})"); + self.inner.insert(key, value) + } + + pub fn remove(&mut self, key: &str) -> Option { + let value = self.inner.remove(key); + + match &value { + Some(value) => debug!("PropertySet::remove({key}) = {value}"), + None => debug!("PropertySet::remove({key}) = None"), + } + + value + } + + pub fn get<'a, V: ExtractFrom<&'a Value>>(&'a self, key: &str) -> Option { + let value = self.inner.get(key); + + match &value { + Some(value) => debug!("PropertySet::get({key}) = {value}"), + None => debug!("PropertySet::get({key}) = None"), + } + + value.and_then(|val| V::extract_from(val, private::Token)) + } + + pub fn iter(&self) -> impl Iterator { + self.inner.iter() + } +} + +impl IntoIterator for PropertySet { + type Item = (Key, Value); + + type IntoIter = alloc::collections::btree_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} + +impl fmt::Debug for PropertySet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} + +macro_rules! impl_from { + ($from:ty => $enum:ident :: $variant:ident) => { + impl From<$from> for $enum { + fn from(value: $from) -> Self { + Self::$variant(value.into()) + } + } + }; +} + +macro_rules! impl_extract_from { + (ref $enum:ident :: as_int => $to:ty) => { + impl ExtractFrom<&$enum> for $to { + fn extract_from(value: &$enum, _token: private::Token) -> Option { + value.as_int().and_then(|v| v.try_into().ok()) + } + } + }; +} + +pub trait ExtractFrom: Sized { + fn extract_from(value: Value, _token: private::Token) -> Option; +} + +/// Represents a value of any type of the .RDP file format. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Value { + /// Numerical value. + Int(i64), + /// String value. + Str(String), +} + +impl Value { + pub fn as_str(&self) -> Option<&str> { + if let Self::Str(value) = self { + Some(value.as_str()) + } else { + None + } + } + + pub fn as_int(&self) -> Option { + if let Self::Int(value) = self { + Some(*value) + } else { + None + } + } +} + +impl Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::Int(value) => write!(f, "{value}"), + Value::Str(value) => write!(f, "\"{value}\""), + } + } +} + +impl_from!(String => Value::Str); +impl_from!(&str => Value::Str); +impl_from!(u8 => Value::Int); +impl_from!(u16 => Value::Int); +impl_from!(u32 => Value::Int); +impl_from!(i8 => Value::Int); +impl_from!(i16 => Value::Int); +impl_from!(i32 => Value::Int); +impl_from!(i64 => Value::Int); +impl_from!(bool => Value::Int); + +impl_extract_from!(ref Value::as_int => u8); +impl_extract_from!(ref Value::as_int => u16); +impl_extract_from!(ref Value::as_int => u32); +impl_extract_from!(ref Value::as_int => i8); +impl_extract_from!(ref Value::as_int => i16); +impl_extract_from!(ref Value::as_int => i32); +impl_extract_from!(ref Value::as_int => i64); + +impl<'a> ExtractFrom<&'a Value> for &'a str { + fn extract_from(value: &'a Value, _token: private::Token) -> Option { + value.as_str() + } +} + +impl ExtractFrom<&Value> for bool { + fn extract_from(value: &Value, _token: private::Token) -> Option { + value.as_int().map(|value| value != 0) + } +} + +mod private { + pub struct Token; +} diff --git a/crates/ironrdp-rdcleanpath/CHANGELOG.md b/crates/ironrdp-rdcleanpath/CHANGELOG.md new file mode 100644 index 00000000..071e3893 --- /dev/null +++ b/crates/ironrdp-rdcleanpath/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdcleanpath-v0.2.0...ironrdp-rdcleanpath-v0.2.1)] - 2025-10-02 + +### Features + +- Human-readable descriptions for RDCleanPath errors (#999) ([18c81ed5d8](https://github.com/Devolutions/IronRDP/commit/18c81ed5d8d3bf13b3d10fe15209233c0c10bb62)) + + More munging to give human-readable webclient-side errors for + RDCleanPath general/negotiation errors, including strings for WSA and + TLS and HTTP error conditions. + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdcleanpath-v0.1.3...ironrdp-rdcleanpath-v0.2.0)] - 2025-08-29 + +### Features + +- [**breaking**] Extend helper API for handling negotiation errors (#930) ([ca11e338d7](https://github.com/Devolutions/IronRDP/commit/ca11e338d7231c86f60a110627a5d864377d8594)) + + - Helper for proxies creating an RDCleanPath error with server response. + - Helper for clients to handle these. + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdcleanpath-v0.1.2...ironrdp-rdcleanpath-v0.1.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdcleanpath-v0.1.1...ironrdp-rdcleanpath-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + diff --git a/crates/ironrdp-rdcleanpath/Cargo.toml b/crates/ironrdp-rdcleanpath/Cargo.toml new file mode 100644 index 00000000..3c3c73dd --- /dev/null +++ b/crates/ironrdp-rdcleanpath/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ironrdp-rdcleanpath" +version = "0.2.1" +readme = "README.md" +description = "RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +der = { version = "0.7", features = ["alloc", "derive"] } # public + +[lints] +workspace = true + diff --git a/crates/ironrdp-rdcleanpath/LICENSE-APACHE b/crates/ironrdp-rdcleanpath/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-rdcleanpath/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-rdcleanpath/LICENSE-MIT b/crates/ironrdp-rdcleanpath/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-rdcleanpath/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-rdcleanpath/README.md b/crates/ironrdp-rdcleanpath/README.md new file mode 100644 index 00000000..30e28de6 --- /dev/null +++ b/crates/ironrdp-rdcleanpath/README.md @@ -0,0 +1,7 @@ +# IronRDP RDCleanPath + +RDCleanPath PDU structure used by IronRDP and Devolutions Gateway. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-rdcleanpath/src/lib.rs b/crates/ironrdp-rdcleanpath/src/lib.rs new file mode 100644 index 00000000..b884ba85 --- /dev/null +++ b/crates/ironrdp-rdcleanpath/src/lib.rs @@ -0,0 +1,533 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +use core::fmt; + +use der::asn1::OctetString; + +// Re-export der crate for convenience +#[rustfmt::skip] // do not re-order this pub use +pub use der; + +pub const BASE_VERSION: u64 = 3389; +pub const VERSION_1: u64 = BASE_VERSION + 1; + +pub const GENERAL_ERROR_CODE: u16 = 1; +pub const NEGOTIATION_ERROR_CODE: u16 = 2; + +#[derive(Clone, Debug, Eq, PartialEq, der::Sequence)] +#[asn1(tag_mode = "EXPLICIT")] +pub struct RDCleanPathErr { + #[asn1(context_specific = "0")] + pub error_code: u16, + #[asn1(context_specific = "1", optional = "true")] + pub http_status_code: Option, + #[asn1(context_specific = "2", optional = "true")] + pub wsa_last_error: Option, + #[asn1(context_specific = "3", optional = "true")] + pub tls_alert_code: Option, +} + +impl fmt::Display for RDCleanPathErr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let error_description = match self.error_code { + GENERAL_ERROR_CODE => "general error", + NEGOTIATION_ERROR_CODE => "negotiation error", + _ => "unknown error", + }; + write!(f, "{error_description} (code {})", self.error_code)?; + + if let Some(http_status_code) = self.http_status_code { + let description = match http_status_code { + 200 => "OK", + 400 => "bad request", + 401 => "unauthorized", + 403 => "forbidden", + 404 => "not found", + 405 => "method not allowed", + 408 => "request timeout", + 409 => "conflict", + 410 => "gone", + 413 => "payload too large", + 414 => "URI too long", + 422 => "unprocessable entity", + 429 => "too many requests", + 500 => "internal server error", + 501 => "not implemented", + 502 => "bad gateway", + 503 => "service unavailable", + 504 => "gateway timeout", + 505 => "HTTP version not supported", + _ => "unknown HTTP status", + }; + write!(f, "; HTTP {http_status_code} {description}")?; + } + + if let Some(wsa_last_error) = self.wsa_last_error { + let description = match wsa_last_error { + 10004 => "interrupted system call", + 10009 => "bad file descriptor", + 10013 => "permission denied", + 10014 => "bad address", + 10022 => "invalid argument", + 10024 => "too many open files", + 10035 => "resource temporarily unavailable", + 10036 => "operation now in progress", + 10037 => "operation already in progress", + 10038 => "socket operation on nonsocket", + 10039 => "destination address required", + 10040 => "message too long", + 10041 => "protocol wrong type for socket", + 10042 => "bad protocol option", + 10043 => "protocol not supported", + 10044 => "socket type not supported", + 10045 => "operation not supported", + 10046 => "protocol family not supported", + 10047 => "address family not supported by protocol family", + 10048 => "address already in use", + 10049 => "cannot assign requested address", + 10050 => "network is down", + 10051 => "network is unreachable", + 10052 => "network dropped connection on reset", + 10053 => "software caused connection abort", + 10054 => "connection reset by peer", + 10055 => "no buffer space available", + 10056 => "socket is already connected", + 10057 => "socket is not connected", + 10058 => "cannot send after socket shutdown", + 10060 => "connection timed out", + 10061 => "connection refused", + 10064 => "host is down", + 10065 => "no route to host", + 10067 => "too many processes", + 10091 => "network subsystem is unavailable", + 10092 => "Winsock version not supported", + 10093 => "successful WSAStartup not yet performed", + 10101 => "graceful shutdown in progress", + 10109 => "class type not found", + 11001 => "host not found", + 11002 => "nonauthoritative host not found", + 11003 => "this is a nonrecoverable error", + 11004 => "valid name, no data record of requested type", + _ => "unknown WSA error", + }; + write!(f, "; WSA {wsa_last_error} {description}")?; + } + + if let Some(tls_alert_code) = self.tls_alert_code { + let description = match tls_alert_code { + 0 => "close notify", + 10 => "unexpected message", + 20 => "bad record MAC", + 21 => "decryption failed", + 22 => "record overflow", + 30 => "decompression failure", + 40 => "handshake failure", + 41 => "no certificate", + 42 => "bad certificate", + 43 => "unsupported certificate", + 44 => "certificate revoked", + 45 => "certificate expired", + 46 => "certificate unknown", + 47 => "illegal parameter", + 48 => "unknown CA", + 49 => "access denied", + 50 => "decode error", + 51 => "decrypt error", + 60 => "export restriction", + 70 => "protocol version", + 71 => "insufficient security", + 80 => "internal error", + 90 => "user canceled", + 100 => "no renegotiation", + 109 => "missing extension", + 110 => "unsupported extension", + 111 => "certificate unobtainable", + 112 => "unrecognized name", + 113 => "bad certificate status response", + 114 => "bad certificate hash value", + 115 => "unknown PSK identity", + 116 => "certificate required", + 120 => "no application protocol", + _ => "unknown TLS alert", + }; + write!(f, "; TLS alert {tls_alert_code} {description}")?; + } + + Ok(()) + } +} + +impl core::error::Error for RDCleanPathErr {} + +#[derive(Clone, Debug, Eq, PartialEq, der::Sequence)] +#[asn1(tag_mode = "EXPLICIT")] +pub struct RDCleanPathPdu { + /// RDCleanPathPdu packet version. + #[asn1(context_specific = "0")] + pub version: u64, + /// The proxy error. + /// + /// Sent from proxy to client only. + #[asn1(context_specific = "1", optional = "true")] + pub error: Option, + /// The RDP server address itself. + /// + /// Sent from client to proxy only. + #[asn1(context_specific = "2", optional = "true")] + pub destination: Option, + /// Arbitrary string for authorization on proxy side. + /// + /// Sent from client to proxy only. + #[asn1(context_specific = "3", optional = "true")] + pub proxy_auth: Option, + /// Currently unused. Could be used by a custom RDP server eventually. + #[asn1(context_specific = "4", optional = "true")] + pub server_auth: Option, + /// The RDP PCB forwarded by the proxy to the RDP server. + /// + /// Sent from client to proxy only. + #[asn1(context_specific = "5", optional = "true")] + pub preconnection_blob: Option, + /// Either the client handshake or the server handshake response. + /// + /// Both client and proxy will set this field. + #[asn1(context_specific = "6", optional = "true")] + pub x224_connection_pdu: Option, + /// The RDP server TLS chain. + /// + /// Sent from proxy to client only. + #[asn1(context_specific = "7", optional = "true")] + pub server_cert_chain: Option>, + // #[asn1(context_specific = "8", optional = "true")] + // pub ocsp_response: Option, + /// IPv4 or IPv6 address of the server found by resolving the destination field on proxy side. + /// + /// Sent from proxy to client only. + #[asn1(context_specific = "9", optional = "true")] + pub server_addr: Option, +} + +impl Default for RDCleanPathPdu { + fn default() -> Self { + Self { + version: VERSION_1, + error: None, + destination: None, + proxy_auth: None, + server_auth: None, + preconnection_blob: None, + x224_connection_pdu: None, + server_cert_chain: None, + server_addr: None, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum DetectionResult { + Detected { version: u64, total_length: usize }, + NotEnoughBytes, + Failed, +} + +impl RDCleanPathPdu { + /// Attempts to decode a RDCleanPath PDU from the provided buffer of bytes. + pub fn from_der(src: &[u8]) -> der::Result { + der::Decode::from_der(src) + } + + /// Try to parse first few bytes in order to detect a RDCleanPath PDU + pub fn detect(src: &[u8]) -> DetectionResult { + use der::{Decode as _, Encode as _}; + + let Ok(mut slice_reader) = der::SliceReader::new(src) else { + return DetectionResult::Failed; + }; + + let header = match der::Header::decode(&mut slice_reader) { + Ok(header) => header, + Err(e) => match e.kind() { + der::ErrorKind::Incomplete { .. } => return DetectionResult::NotEnoughBytes, + _ => return DetectionResult::Failed, + }, + }; + + let (Ok(header_encoded_len), Ok(body_length)) = ( + header.encoded_len().and_then(usize::try_from), + usize::try_from(header.length), + ) else { + return DetectionResult::Failed; + }; + + let Some(total_length) = header_encoded_len.checked_add(body_length) else { + return DetectionResult::Failed; + }; + + match der::asn1::ContextSpecific::::decode_explicit(&mut slice_reader, der::TagNumber::N0) { + Ok(Some(version)) if version.value == VERSION_1 => DetectionResult::Detected { + version: VERSION_1, + total_length, + }, + Ok(Some(_)) => DetectionResult::Failed, + Ok(None) => DetectionResult::NotEnoughBytes, + Err(e) => match e.kind() { + der::ErrorKind::Incomplete { .. } => DetectionResult::NotEnoughBytes, + _ => DetectionResult::Failed, + }, + } + } + + pub fn into_enum(self) -> Result { + RDCleanPath::try_from(self) + } + + pub fn new_general_error() -> Self { + Self { + version: VERSION_1, + error: Some(RDCleanPathErr { + error_code: GENERAL_ERROR_CODE, + http_status_code: None, + wsa_last_error: None, + tls_alert_code: None, + }), + ..Self::default() + } + } + + pub fn new_http_error(status_code: u16) -> Self { + Self { + version: VERSION_1, + error: Some(RDCleanPathErr { + error_code: GENERAL_ERROR_CODE, + http_status_code: Some(status_code), + wsa_last_error: None, + tls_alert_code: None, + }), + ..Self::default() + } + } + + pub fn new_request( + x224_pdu: Vec, + destination: String, + proxy_auth: String, + pcb: Option, + ) -> der::Result { + Ok(Self { + version: VERSION_1, + destination: Some(destination), + proxy_auth: Some(proxy_auth), + preconnection_blob: pcb, + x224_connection_pdu: Some(OctetString::new(x224_pdu)?), + ..Self::default() + }) + } + + pub fn new_response( + server_addr: String, + x224_pdu: Vec, + x509_chain: impl IntoIterator>, + ) -> der::Result { + Ok(Self { + version: VERSION_1, + x224_connection_pdu: Some(OctetString::new(x224_pdu)?), + server_cert_chain: Some( + x509_chain + .into_iter() + .map(OctetString::new) + .collect::>()?, + ), + server_addr: Some(server_addr), + ..Self::default() + }) + } + + pub fn new_tls_error(alert_code: u8) -> Self { + Self { + version: VERSION_1, + error: Some(RDCleanPathErr { + error_code: GENERAL_ERROR_CODE, + http_status_code: None, + wsa_last_error: None, + tls_alert_code: Some(alert_code), + }), + ..Self::default() + } + } + + pub fn new_wsa_error(wsa_error_code: u16) -> Self { + Self { + version: VERSION_1, + error: Some(RDCleanPathErr { + error_code: GENERAL_ERROR_CODE, + http_status_code: None, + wsa_last_error: Some(wsa_error_code), + tls_alert_code: None, + }), + ..Self::default() + } + } + + /// Creates a negotiation error response that includes the server's X.224 negotiation response. + /// + /// This allows clients to extract specific negotiation failure details + /// (like "CredSSP required") from the server's original response. + /// + /// # Example + /// ```rust + /// use ironrdp_rdcleanpath::RDCleanPathPdu; + /// + /// // Server rejected connection with "CredSSP required" - preserve this info + /// let server_response = vec![/* X.224 Connection Confirm with failure code */]; + /// let error_pdu = RDCleanPathPdu::new_negotiation_error(server_response)?; + /// # Ok::<(), der::Error>(()) + /// ``` + pub fn new_negotiation_error(server_x224_response: Vec) -> der::Result { + Ok(Self { + version: VERSION_1, + error: Some(RDCleanPathErr { + error_code: NEGOTIATION_ERROR_CODE, + http_status_code: None, + wsa_last_error: None, + tls_alert_code: None, + }), + x224_connection_pdu: Some(OctetString::new(server_x224_response)?), + ..Self::default() + }) + } + + pub fn to_der(&self) -> der::Result> { + der::Encode::to_der(self) + } +} + +/// Helper enum to leverage Rust pattern matching feature. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RDCleanPath { + Request { + destination: String, + proxy_auth: String, + server_auth: Option, + preconnection_blob: Option, + x224_connection_request: OctetString, + }, + Response { + x224_connection_response: OctetString, + server_cert_chain: Vec, + server_addr: String, + }, + GeneralErr(RDCleanPathErr), + NegotiationErr { + x224_connection_response: Vec, + }, +} + +impl RDCleanPath { + pub fn into_pdu(self) -> RDCleanPathPdu { + RDCleanPathPdu::from(self) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MissingRDCleanPathField(&'static str); + +impl fmt::Display for MissingRDCleanPathField { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "RDCleanPath is missing {} field", self.0) + } +} + +impl core::error::Error for MissingRDCleanPathField {} + +impl TryFrom for RDCleanPath { + type Error = MissingRDCleanPathField; + + fn try_from(pdu: RDCleanPathPdu) -> Result { + let rdcleanpath = if let Some(destination) = pdu.destination { + Self::Request { + destination, + proxy_auth: pdu.proxy_auth.ok_or(MissingRDCleanPathField("proxy_auth"))?, + server_auth: pdu.server_auth, + preconnection_blob: pdu.preconnection_blob, + x224_connection_request: pdu + .x224_connection_pdu + .ok_or(MissingRDCleanPathField("x224_connection_pdu"))?, + } + } else if let Some(server_addr) = pdu.server_addr { + Self::Response { + x224_connection_response: pdu + .x224_connection_pdu + .ok_or(MissingRDCleanPathField("x224_connection_pdu"))?, + server_cert_chain: pdu + .server_cert_chain + .ok_or(MissingRDCleanPathField("server_cert_chain"))?, + server_addr, + } + } else { + let error = pdu.error.ok_or(MissingRDCleanPathField("error"))?; + match (error.error_code, pdu.x224_connection_pdu) { + (NEGOTIATION_ERROR_CODE, Some(x224_pdu)) => Self::NegotiationErr { + x224_connection_response: x224_pdu.as_bytes().to_vec(), + }, + _ => Self::GeneralErr(error), + } + }; + + Ok(rdcleanpath) + } +} + +impl From for RDCleanPathPdu { + fn from(value: RDCleanPath) -> Self { + match value { + RDCleanPath::Request { + destination, + proxy_auth, + server_auth, + preconnection_blob, + x224_connection_request, + } => Self { + version: VERSION_1, + destination: Some(destination), + proxy_auth: Some(proxy_auth), + server_auth, + preconnection_blob, + x224_connection_pdu: Some(x224_connection_request), + ..Default::default() + }, + RDCleanPath::Response { + x224_connection_response, + server_cert_chain, + server_addr, + } => Self { + version: VERSION_1, + x224_connection_pdu: Some(x224_connection_response), + server_cert_chain: Some(server_cert_chain), + server_addr: Some(server_addr), + ..Default::default() + }, + RDCleanPath::GeneralErr(error) => Self { + version: VERSION_1, + error: Some(error), + ..Default::default() + }, + RDCleanPath::NegotiationErr { + x224_connection_response, + } => Self { + version: VERSION_1, + error: Some(RDCleanPathErr { + error_code: NEGOTIATION_ERROR_CODE, + http_status_code: None, + wsa_last_error: None, + tls_alert_code: None, + }), + x224_connection_pdu: Some( + OctetString::new(x224_connection_response) + .expect("x224_connection_response smaller than u32::MAX (256 MiB)"), + ), + ..Default::default() + }, + } + } +} diff --git a/crates/ironrdp-rdpdr-native/CHANGELOG.md b/crates/ironrdp-rdpdr-native/CHANGELOG.md new file mode 100644 index 00000000..301fa404 --- /dev/null +++ b/crates/ironrdp-rdpdr-native/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-native-v0.4.0...ironrdp-rdpdr-native-v0.5.0)] - 2025-12-18 + + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-native-v0.3.0...ironrdp-rdpdr-native-v0.4.0)] - 2025-08-29 + +### Build + +- Bump nix to 0.30 ([971ad922a5](https://github.com/Devolutions/IronRDP/commit/971ad922a51f78511243aaa885acdd8b1ed94b27)) +- Bump ironrdp-pdu + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-native-v0.1.2...ironrdp-rdpdr-native-v0.2.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-native-v0.1.1...ironrdp-rdpdr-native-v0.1.2)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) diff --git a/crates/ironrdp-rdpdr-native/Cargo.toml b/crates/ironrdp-rdpdr-native/Cargo.toml new file mode 100644 index 00000000..14fea584 --- /dev/null +++ b/crates/ironrdp-rdpdr-native/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ironrdp-rdpdr-native" +version = "0.5.0" +readme = "README.md" +description = "Native RDPDR static channel backend implementations for IronRDP" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.5" } # public +nix = { version = "0.30", features = ["fs", "dir"] } +tracing = { version = "0.1", features = ["log"] } diff --git a/crates/ironrdp-rdpdr-native/LICENSE-APACHE b/crates/ironrdp-rdpdr-native/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-rdpdr-native/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-rdpdr-native/LICENSE-MIT b/crates/ironrdp-rdpdr-native/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-rdpdr-native/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-rdpdr-native/README.md b/crates/ironrdp-rdpdr-native/README.md new file mode 100644 index 00000000..27447773 --- /dev/null +++ b/crates/ironrdp-rdpdr-native/README.md @@ -0,0 +1,3 @@ +# IronRDP RDPDR native backends + +Native RDPDR backend implementations. Currently only *nix systems are supported. \ No newline at end of file diff --git a/crates/ironrdp-rdpdr-native/src/lib.rs b/crates/ironrdp-rdpdr-native/src/lib.rs new file mode 100644 index 00000000..3b1ffa83 --- /dev/null +++ b/crates/ironrdp-rdpdr-native/src/lib.rs @@ -0,0 +1,15 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![warn(unsafe_op_in_unsafe_fn)] +#![warn(invalid_reference_casting)] +#![warn(clippy::undocumented_unsafe_blocks)] +#![warn(clippy::multiple_unsafe_ops_per_block)] +#![warn(clippy::transmute_ptr_to_ptr)] +#![warn(clippy::as_ptr_cast_mut)] +#![warn(clippy::cast_ptr_alignment)] +#![warn(clippy::fn_to_numeric_cast_any)] +#![warn(clippy::ptr_cast_constness)] + +#[cfg(any(target_os = "macos", target_os = "linux"))] +mod nix; +#[cfg(any(target_os = "macos", target_os = "linux"))] +pub use nix::backend; diff --git a/crates/ironrdp-rdpdr-native/src/nix/backend.rs b/crates/ironrdp-rdpdr-native/src/nix/backend.rs new file mode 100644 index 00000000..30721497 --- /dev/null +++ b/crates/ironrdp-rdpdr-native/src/nix/backend.rs @@ -0,0 +1,783 @@ +use std::ffi::CString; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::os::fd::{AsFd, AsRawFd}; +use std::os::unix::fs::MetadataExt; + +use ironrdp_core::impl_as_any; +use ironrdp_pdu::{encode_err, PduResult}; +use ironrdp_rdpdr::pdu::efs::*; +use ironrdp_rdpdr::pdu::esc::{ScardCall, ScardIoCtlCode}; +use ironrdp_rdpdr::pdu::RdpdrPdu; +use ironrdp_rdpdr::RdpdrBackend; +use ironrdp_svc::SvcMessage; +use nix::dir::{Dir, OwningIter}; +use tracing::{debug, warn}; + +#[derive(Debug, Default)] +pub struct NixRdpdrBackend { + file_id: u32, + file_base: String, + file_map: std::collections::HashMap, + file_path_map: std::collections::HashMap, + file_dir_map: std::collections::HashMap, +} + +impl NixRdpdrBackend { + pub fn new(file_base: String) -> Self { + Self { + file_base, + ..Default::default() + } + } +} + +impl_as_any!(NixRdpdrBackend); + +impl RdpdrBackend for NixRdpdrBackend { + fn handle_server_device_announce_response(&mut self, _pdu: ServerDeviceAnnounceResponse) -> PduResult<()> { + Ok(()) + } + fn handle_scard_call(&mut self, _req: DeviceControlRequest, _call: ScardCall) -> PduResult<()> { + Ok(()) + } + fn handle_drive_io_request(&mut self, req: ServerDriveIoRequest) -> PduResult> { + debug!("handle_drive_io_request:{:?}", req); + match req { + ServerDriveIoRequest::DeviceWriteRequest(req_inner) => write_device(self, req_inner), + ServerDriveIoRequest::ServerCreateDriveRequest(req_inner) => create_drive(self, req_inner), + ServerDriveIoRequest::DeviceReadRequest(req_inner) => read_device(self, req_inner), + ServerDriveIoRequest::DeviceCloseRequest(req_inner) => close_device(self, req_inner), + ServerDriveIoRequest::ServerDriveNotifyChangeDirectoryRequest(_) => { + // TODO + Ok(Vec::new()) + } + ServerDriveIoRequest::ServerDriveQueryDirectoryRequest(req_inner) => query_directory(self, req_inner), + ServerDriveIoRequest::ServerDriveQueryInformationRequest(req_inner) => query_information(self, req_inner), + ServerDriveIoRequest::ServerDriveQueryVolumeInformationRequest(req_inner) => { + query_volume_information(self, req_inner) + } + ServerDriveIoRequest::ServerDriveSetInformationRequest(req_inner) => set_information(self, req_inner), + ServerDriveIoRequest::DeviceControlRequest(req_inner) => Ok(vec![SvcMessage::from( + RdpdrPdu::DeviceControlResponse(DeviceControlResponse { + device_io_reply: DeviceIoResponse::new(req_inner.header, NtStatus::SUCCESS), + output_buffer: None, + }), + )]), + ServerDriveIoRequest::ServerDriveLockControlRequest(_) => { + // TODO + Ok(Vec::new()) + } + } + } +} + +pub(crate) fn write_device(backend: &mut NixRdpdrBackend, req_inner: DeviceWriteRequest) -> PduResult> { + return process_dependent_file( + backend, + req_inner.device_io_request, + |request| { + let res = RdpdrPdu::DeviceWriteResponse(DeviceWriteResponse { + device_io_reply: DeviceIoResponse::new(request, NtStatus::NO_SUCH_FILE), + length: 0u32, + }); + Ok(vec![SvcMessage::from(res)]) + }, + |file, request| match write_inner(file, req_inner.offset, &req_inner.write_data) { + Ok(length) => { + if length == req_inner.write_data.len() { + Ok(vec![SvcMessage::from(RdpdrPdu::DeviceWriteResponse( + DeviceWriteResponse { + device_io_reply: DeviceIoResponse::new(request, NtStatus::SUCCESS), + length: u32::try_from(req_inner.write_data.len()).unwrap(), + }, + ))]) + } else { + warn!( + "Written content len:{} is not equal to {}", + length, + req_inner.write_data.len() + ); + let res = RdpdrPdu::DeviceWriteResponse(DeviceWriteResponse { + device_io_reply: DeviceIoResponse::new(request, NtStatus::UNSUCCESSFUL), + length: 0u32, + }); + Ok(vec![SvcMessage::from(res)]) + } + } + Err(error) => { + warn!(%error, "Write error"); + let res = RdpdrPdu::DeviceWriteResponse(DeviceWriteResponse { + device_io_reply: DeviceIoResponse::new(request, NtStatus::UNSUCCESSFUL), + length: 0u32, + }); + Ok(vec![SvcMessage::from(res)]) + } + }, + ); + fn write_inner(file: &mut std::fs::File, offset: u64, write_data: &[u8]) -> std::io::Result { + let sf = SeekFrom::Start(offset); + file.seek(sf)?; + let length = file.write(write_data)?; + file.flush()?; + Ok(length) + } +} + +pub(crate) fn read_device(backend: &mut NixRdpdrBackend, req_inner: DeviceReadRequest) -> PduResult> { + return process_dependent_file( + backend, + req_inner.device_io_request, + |request| { + let res = RdpdrPdu::DeviceReadResponse(DeviceReadResponse { + device_io_reply: DeviceIoResponse::new(request, NtStatus::NO_SUCH_FILE), + read_data: Vec::new(), + }); + Ok(vec![SvcMessage::from(res)]) + }, + |file, request| match read_inner(file, req_inner.offset, usize::try_from(req_inner.length).unwrap()) { + Ok(buf) => { + let res = RdpdrPdu::DeviceReadResponse(DeviceReadResponse { + device_io_reply: DeviceIoResponse::new(request, NtStatus::SUCCESS), + read_data: buf, + }); + Ok(vec![SvcMessage::from(res)]) + } + Err(error) => { + warn!(?error, "Read error"); + let res = RdpdrPdu::DeviceReadResponse(DeviceReadResponse { + device_io_reply: DeviceIoResponse::new(request, NtStatus::UNSUCCESSFUL), + read_data: Vec::new(), + }); + Ok(vec![SvcMessage::from(res)]) + } + }, + ); + fn read_inner(file: &mut std::fs::File, offset: u64, length: usize) -> std::io::Result> { + let sf = SeekFrom::Start(offset); + file.seek(sf)?; + let mut buf = vec![0; length]; + + let length = file.read(&mut buf)?; + buf.resize(length, 0u8); + Ok(buf) + } +} + +pub(crate) fn close_device(backend: &mut NixRdpdrBackend, req_inner: DeviceCloseRequest) -> PduResult> { + backend.file_map.remove(&req_inner.device_io_request.file_id); + backend.file_path_map.remove(&req_inner.device_io_request.file_id); + backend.file_dir_map.remove(&req_inner.device_io_request.file_id); + let res = RdpdrPdu::DeviceCloseResponse(DeviceCloseResponse { + device_io_response: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::SUCCESS), + }); + Ok(vec![SvcMessage::from(res)]) +} + +pub(crate) fn query_information( + backend: &mut NixRdpdrBackend, + req_inner: ServerDriveQueryInformationRequest, +) -> PduResult> { + match backend.file_map.get(&req_inner.device_io_request.file_id) { + Some(file) => match file.metadata() { + Ok(meta) => { + let path = backend + .file_path_map + .get(&req_inner.device_io_request.file_id) + .cloned() + .unwrap_or_default(); + let name_index = match path.rfind('/') { + // in fact, index only needs to be different for existing requests + #[expect(clippy::arithmetic_side_effects)] + Some(index) => index + 1, + None => 0, + }; + let name = &path[name_index..]; + let file_attribute = get_file_attributes(&meta, name); + if FileInformationClassLevel::FILE_BASIC_INFORMATION == req_inner.file_info_class_lvl { + let basic_info = FileBasicInformation { + creation_time: transform_to_filetime(meta.ctime()), + last_access_time: transform_to_filetime(meta.atime()), + last_write_time: transform_to_filetime(meta.mtime()), + change_time: transform_to_filetime(meta.ctime()), + file_attributes: file_attribute, + }; + let res = RdpdrPdu::ClientDriveQueryInformationResponse(ClientDriveQueryInformationResponse { + device_io_response: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::SUCCESS), + buffer: Some(FileInformationClass::Basic(basic_info)), + }); + Ok(vec![SvcMessage::from(res)]) + } else if FileInformationClassLevel::FILE_STANDARD_INFORMATION == req_inner.file_info_class_lvl { + let dir = if meta.is_dir() { Boolean::True } else { Boolean::False }; + let standard_info = FileStandardInformation { + allocation_size: i64::try_from(meta.size()).unwrap(), + end_of_file: i64::try_from(meta.size()).unwrap(), + number_of_links: u32::try_from(meta.nlink()).unwrap(), + delete_pending: Boolean::False, + directory: dir, + }; + let res = RdpdrPdu::ClientDriveQueryInformationResponse(ClientDriveQueryInformationResponse { + device_io_response: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::SUCCESS), + buffer: Some(FileInformationClass::Standard(standard_info)), + }); + Ok(vec![SvcMessage::from(res)]) + } else if FileInformationClassLevel::FILE_ATTRIBUTE_TAG_INFORMATION == req_inner.file_info_class_lvl { + let info = FileAttributeTagInformation { + file_attributes: file_attribute, + reparse_tag: 0, + }; + let res = RdpdrPdu::ClientDriveQueryInformationResponse(ClientDriveQueryInformationResponse { + device_io_response: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::SUCCESS), + buffer: Some(FileInformationClass::AttributeTag(info)), + }); + Ok(vec![SvcMessage::from(res)]) + } else { + warn!("unsupported file class"); + let res = RdpdrPdu::ClientDriveQueryInformationResponse(ClientDriveQueryInformationResponse { + device_io_response: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::UNSUCCESSFUL), + buffer: None, + }); + Ok(vec![SvcMessage::from(res)]) + } + } + Err(error) => { + warn!(?error, "Get file metadata error"); + let res = RdpdrPdu::ClientDriveQueryInformationResponse(ClientDriveQueryInformationResponse { + device_io_response: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::UNSUCCESSFUL), + buffer: None, + }); + Ok(vec![SvcMessage::from(res)]) + } + }, + None => { + warn!("no such file"); + let res = RdpdrPdu::ClientDriveQueryInformationResponse(ClientDriveQueryInformationResponse { + device_io_response: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::NO_SUCH_FILE), + buffer: None, + }); + Ok(vec![SvcMessage::from(res)]) + } + } +} + +pub(crate) fn query_volume_information( + backend: &mut NixRdpdrBackend, + req_inner: ServerDriveQueryVolumeInformationRequest, +) -> PduResult> { + match backend.file_map.get(&req_inner.device_io_request.file_id) { + Some(file) => { + if let Ok(statvfs) = nix::sys::statvfs::fstatvfs(file.as_fd()) { + if FileSystemInformationClassLevel::FILE_FS_FULL_SIZE_INFORMATION == req_inner.fs_info_class_lvl { + #[cfg_attr(target_os = "macos", expect(clippy::unnecessary_fallible_conversions))] + let info = FileFsFullSizeInformation { + total_alloc_units: i64::try_from(statvfs.blocks()).unwrap(), + caller_available_alloc_units: i64::try_from(statvfs.blocks_available()).unwrap(), + actual_available_alloc_units: i64::try_from(statvfs.blocks_available()).unwrap(), + sectors_per_alloc_unit: u32::try_from(statvfs.fragment_size()).unwrap(), + bytes_per_sector: 1, + }; + + Ok(vec![SvcMessage::from( + RdpdrPdu::ClientDriveQueryVolumeInformationResponse( + ClientDriveQueryVolumeInformationResponse { + device_io_reply: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::SUCCESS), + buffer: Some(FileSystemInformationClass::FileFsFullSizeInformation(info)), + }, + ), + )]) + } else if FileSystemInformationClassLevel::FILE_FS_ATTRIBUTE_INFORMATION == req_inner.fs_info_class_lvl + { + Ok(vec![SvcMessage::from( + RdpdrPdu::ClientDriveQueryVolumeInformationResponse( + ClientDriveQueryVolumeInformationResponse { + device_io_reply: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::SUCCESS), + buffer: Some(FileSystemInformationClass::FileFsAttributeInformation( + FileFsAttributeInformation { + file_system_attributes: FileSystemAttributes::FILE_CASE_SENSITIVE_SEARCH + | FileSystemAttributes::FILE_CASE_PRESERVED_NAMES + | FileSystemAttributes::FILE_UNICODE_ON_DISK, + max_component_name_len: 260, + file_system_name: "FAT32".to_owned(), + }, + )), + }, + ), + )]) + } else if FileSystemInformationClassLevel::FILE_FS_VOLUME_INFORMATION == req_inner.fs_info_class_lvl { + Ok(vec![SvcMessage::from( + RdpdrPdu::ClientDriveQueryVolumeInformationResponse( + ClientDriveQueryVolumeInformationResponse { + device_io_reply: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::SUCCESS), + buffer: Some(FileSystemInformationClass::FileFsVolumeInformation( + FileFsVolumeInformation { + volume_creation_time: transform_to_filetime(file.metadata().unwrap().ctime()), + // blocks_available() may have different integer type on different platforms. + // so we need to cast it to u32 uniformly. so if it is u32, it will emit 'useless conversion' + // warning, i choose to mute it. + #[expect( + clippy::allow_attributes, + reason = "we have to use allow as the useless_conversion isn't triggered on some platforms" + )] + #[allow(clippy::useless_conversion)] + volume_serial_number: u32::try_from(statvfs.blocks_available()).unwrap(), + supports_objects: Boolean::False, + volume_label: "IRON_RDP".to_owned(), + }, + )), + }, + ), + )]) + } else if FileSystemInformationClassLevel::FILE_FS_SIZE_INFORMATION == req_inner.fs_info_class_lvl { + Ok(vec![SvcMessage::from( + RdpdrPdu::ClientDriveQueryVolumeInformationResponse( + ClientDriveQueryVolumeInformationResponse { + device_io_reply: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::SUCCESS), + #[cfg_attr(target_os = "macos", expect(clippy::unnecessary_fallible_conversions))] + buffer: Some(FileSystemInformationClass::FileFsSizeInformation( + FileFsSizeInformation { + total_alloc_units: i64::try_from(statvfs.blocks()).unwrap(), + available_alloc_units: i64::try_from(statvfs.blocks_free()).unwrap(), + sectors_per_alloc_unit: u32::try_from(statvfs.fragment_size()).unwrap(), + bytes_per_sector: 1, + }, + )), + }, + ), + )]) + } else { + warn!("unsupported volume class"); + Ok(vec![SvcMessage::from( + RdpdrPdu::ClientDriveQueryVolumeInformationResponse( + ClientDriveQueryVolumeInformationResponse { + device_io_reply: DeviceIoResponse::new( + req_inner.device_io_request, + NtStatus::UNSUCCESSFUL, + ), + buffer: None, + }, + ), + )]) + } + } else { + warn!("no such file"); + let res = RdpdrPdu::ClientDriveQueryInformationResponse(ClientDriveQueryInformationResponse { + device_io_response: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::NO_SUCH_FILE), + buffer: None, + }); + Ok(vec![SvcMessage::from(res)]) + } + } + None => { + warn!("no such file"); + let res = RdpdrPdu::ClientDriveQueryInformationResponse(ClientDriveQueryInformationResponse { + device_io_response: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::NO_SUCH_FILE), + buffer: None, + }); + Ok(vec![SvcMessage::from(res)]) + } + } +} + +pub(crate) fn set_information( + backend: &mut NixRdpdrBackend, + req_inner: ServerDriveSetInformationRequest, +) -> PduResult> { + match backend.file_path_map.get(&req_inner.device_io_request.file_id) { + Some(file) => { + match &req_inner.set_buffer { + FileInformationClass::Rename(info) => { + let mut to = backend.file_base.clone(); + to.push_str(&info.file_name.replace('\\', "/")); + if let Err(error) = std::fs::rename(file, to) { + warn!(?error, "Rename file error"); + let res = RdpdrPdu::ClientDriveSetInformationResponse( + ClientDriveSetInformationResponse::new(&req_inner, NtStatus::UNSUCCESSFUL) + .map_err(|e| encode_err!(e))?, + ); + return Ok(vec![SvcMessage::from(res)]); + } + } + FileInformationClass::Allocation(_) => { + //nothing to do + } + FileInformationClass::Disposition(_) => { + if let Err(error) = std::fs::remove_file(file) { + warn!(?error, "Remove file error"); + let res = RdpdrPdu::ClientDriveSetInformationResponse( + ClientDriveSetInformationResponse::new(&req_inner, NtStatus::UNSUCCESSFUL) + .map_err(|e| encode_err!(e))?, + ); + return Ok(vec![SvcMessage::from(res)]); + } + } + FileInformationClass::EndOfFile(info) => { + if let Some(file) = backend.file_map.get(&req_inner.device_io_request.file_id) { + // SAFETY: the file must has been opened with write access in the last steps, since rdp prepares to set information. In addition it is a regular file. + let set_end_res = unsafe { nix::libc::ftruncate(file.as_raw_fd(), info.end_of_file) }; + if set_end_res < 0 { + let error = nix::errno::Errno::last(); + warn!(%error, "Failed to set end of file"); + let res = RdpdrPdu::ClientDriveSetInformationResponse( + ClientDriveSetInformationResponse::new(&req_inner, NtStatus::UNSUCCESSFUL) + .map_err(|e| encode_err!(e))?, + ); + return Ok(vec![SvcMessage::from(res)]); + } + } else { + warn!("no such file"); + let res = RdpdrPdu::ClientDriveSetInformationResponse( + ClientDriveSetInformationResponse::new(&req_inner, NtStatus::NO_SUCH_FILE) + .map_err(|e| encode_err!(e))?, + ); + return Ok(vec![SvcMessage::from(res)]); + } + } + _ => { + // TODO + } + } + } + None => { + warn!("no such file"); + let res = RdpdrPdu::ClientDriveSetInformationResponse( + ClientDriveSetInformationResponse::new(&req_inner, NtStatus::NO_SUCH_FILE) + .map_err(|e| encode_err!(e))?, + ); + return Ok(vec![SvcMessage::from(res)]); + } + } + Ok(vec![SvcMessage::from(RdpdrPdu::ClientDriveSetInformationResponse( + ClientDriveSetInformationResponse::new(&req_inner, NtStatus::SUCCESS).map_err(|e| encode_err!(e))?, + ))]) +} + +// in fact, it is time in secs which is very small +#[expect(clippy::arithmetic_side_effects)] +pub(crate) fn transform_to_filetime(time_in_secs: i64) -> i64 { + let mut time = time_in_secs * 10000000; + time += 116444736000000000; + time +} + +pub(crate) fn get_file_attributes(meta: &std::fs::Metadata, file_name: &str) -> FileAttributes { + let mut file_attribute = FileAttributes::empty(); + if meta.is_dir() { + file_attribute |= FileAttributes::FILE_ATTRIBUTE_DIRECTORY; + } + if file_attribute.is_empty() { + file_attribute |= FileAttributes::FILE_ATTRIBUTE_ARCHIVE; + } + + if file_name.len() > 1 && file_name.starts_with('.') && file_name.as_bytes()[1] != b'.' { + file_attribute |= FileAttributes::FILE_ATTRIBUTE_HIDDEN; + } + if meta.permissions().readonly() { + file_attribute |= FileAttributes::FILE_ATTRIBUTE_READONLY; + } + file_attribute +} + +pub(crate) fn make_query_dir_resp( + find_file_name: Option, + device_io_request: DeviceIoRequest, + file_class: FileInformationClassLevel, + initial_query: bool, +) -> PduResult> { + let not_found_status = if initial_query { + NtStatus::NO_SUCH_FILE + } else { + NtStatus::NO_MORE_FILES + }; + match find_file_name { + None => Ok(vec![SvcMessage::from(RdpdrPdu::ClientDriveQueryDirectoryResponse( + ClientDriveQueryDirectoryResponse { + device_io_reply: DeviceIoResponse::new(device_io_request, not_found_status), + buffer: None, + }, + ))]), + Some(file_full_path) => { + // in fact, it represents file name, so it is not very large + #[expect(clippy::arithmetic_side_effects)] + let file_last_slash = if let Some(index) = file_full_path.rfind('/') { + index + 1 + } else { + 0 + }; + let file_name = &file_full_path[file_last_slash..]; + match std::fs::metadata(&file_full_path) { + Ok(meta) => { + let file_attribute = get_file_attributes(&meta, file_name); + if file_class == FileInformationClassLevel::FILE_BOTH_DIRECTORY_INFORMATION { + let info = FileBothDirectoryInformation::new( + transform_to_filetime(meta.ctime()), + transform_to_filetime(meta.ctime()), + transform_to_filetime(meta.atime()), + transform_to_filetime(meta.mtime()), + i64::try_from(meta.size()).unwrap(), + file_attribute, + file_name.to_owned(), + ); + let info2 = FileInformationClass::BothDirectory(info); + Ok(vec![SvcMessage::from(RdpdrPdu::ClientDriveQueryDirectoryResponse( + ClientDriveQueryDirectoryResponse { + device_io_reply: DeviceIoResponse::new(device_io_request, NtStatus::SUCCESS), + buffer: Some(info2), + }, + ))]) + } else { + warn!("unsupported file class for query directory"); + Ok(vec![SvcMessage::from(RdpdrPdu::ClientDriveQueryDirectoryResponse( + ClientDriveQueryDirectoryResponse { + device_io_reply: DeviceIoResponse::new(device_io_request, NtStatus::NOT_SUPPORTED), + buffer: None, + }, + ))]) + } + } + Err(error) => { + warn!(%error, "Get metadata error"); + Ok(vec![SvcMessage::from(RdpdrPdu::ClientDriveQueryDirectoryResponse( + ClientDriveQueryDirectoryResponse { + device_io_reply: DeviceIoResponse::new(device_io_request, not_found_status), + buffer: None, + }, + ))]) + } + } + } + } +} + +pub(crate) fn query_directory( + backend: &mut NixRdpdrBackend, + req_inner: ServerDriveQueryDirectoryRequest, +) -> PduResult> { + match backend.file_path_map.get(&req_inner.device_io_request.file_id) { + Some(parent_pos_for_next) => { + let mut find_file_name = None; + if req_inner.initial_query > 0 { + if req_inner.path.ends_with('*') { + let mut parent = backend.file_base.clone(); + let query_path = req_inner.path.replace('\\', "/"); + let len = query_path.len(); + // path ends with *, so its len > 0 + #[expect(clippy::arithmetic_side_effects)] + parent.push_str(&query_path[0..len - 1]); + if let Ok(dirp) = Dir::open( + parent.as_str(), + nix::fcntl::OFlag::O_RDONLY, + nix::sys::stat::Mode::empty(), + ) { + let mut iter = dirp.into_iter(); + while let Some(Ok(first)) = iter.next() { + let file_name = first.file_name(); + if CString::new(".").unwrap().as_c_str() == file_name + || CString::new("..").unwrap().as_c_str() == file_name + { + continue; + } + parent.push_str(file_name.to_string_lossy().into_owned().as_str()); + find_file_name = Some(parent); + break; + } + backend.file_dir_map.insert(req_inner.device_io_request.file_id, iter); + } + } else { + let mut full_path = backend.file_base.clone(); + let query_path = req_inner.path.replace('\\', "/"); + full_path.push_str(&query_path); + find_file_name = Some(full_path); + } + make_query_dir_resp( + find_file_name, + req_inner.device_io_request, + req_inner.file_info_class_lvl, + true, + ) + } else { + if let Some(dirp_iter) = backend.file_dir_map.get_mut(&req_inner.device_io_request.file_id) { + if let Some(Ok(next)) = dirp_iter.next() { + let file_name = next.file_name(); + let mut full_path = parent_pos_for_next.clone(); + if !full_path.ends_with('/') { + full_path.push('/'); + } + full_path.push_str(file_name.to_string_lossy().into_owned().as_str()); + find_file_name = Some(full_path); + } + } + make_query_dir_resp( + find_file_name, + req_inner.device_io_request, + req_inner.file_info_class_lvl, + false, + ) + } + } + None => { + warn!("no file to query directory"); + Ok(vec![SvcMessage::from(RdpdrPdu::ClientDriveQueryDirectoryResponse( + ClientDriveQueryDirectoryResponse { + device_io_reply: DeviceIoResponse::new(req_inner.device_io_request, NtStatus::NO_SUCH_FILE), + buffer: None, + }, + ))]) + } + } +} + +fn make_create_drive_resp( + device_io_request: DeviceIoRequest, + create_disposation: CreateDisposition, + file_id: u32, +) -> PduResult> { + let io_response = DeviceIoResponse::new(device_io_request, NtStatus::SUCCESS); + let information = match create_disposation { + CreateDisposition::FILE_CREATE + | CreateDisposition::FILE_SUPERSEDE + | CreateDisposition::FILE_OPEN + | CreateDisposition::FILE_OVERWRITE => Information::FILE_SUPERSEDED, + CreateDisposition::FILE_OPEN_IF => Information::FILE_OPENED, + CreateDisposition::FILE_OVERWRITE_IF => Information::FILE_OVERWRITTEN, + _ => Information::empty(), + }; + let res = RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse { + device_io_reply: io_response, + file_id, + information, + }); + Ok(vec![SvcMessage::from(res)]) +} +// in fact, index only needs to be different, so it is ok +#[expect(clippy::arithmetic_side_effects)] +pub(crate) fn create_drive( + backend: &mut NixRdpdrBackend, + req_inner: DeviceCreateRequest, +) -> PduResult> { + let file_id = backend.file_id; + backend.file_id += 1; + let mut path = String::from(backend.file_base.as_str()); + path.push_str(&req_inner.path.replace('\\', "/")); + // first process directory + match std::fs::metadata(&path) { + Ok(meta) => { + if meta.is_dir() { + if req_inner.create_disposition == CreateDisposition::FILE_CREATE { + warn!("Attempt to create directory, but it exists"); + let io_response = DeviceIoResponse::new(req_inner.device_io_request, NtStatus::UNSUCCESSFUL); + let res = RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse { + device_io_reply: io_response, + file_id, + information: Information::empty(), + }); + return Ok(vec![SvcMessage::from(res)]); + } + if req_inner.create_options.bits() & CreateOptions::FILE_NON_DIRECTORY_FILE.bits() != 0 { + warn!("Attempt to create a file, but it is a directory"); + let io_response = DeviceIoResponse::new(req_inner.device_io_request, NtStatus::UNSUCCESSFUL); + let res = RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse { + device_io_reply: io_response, + file_id, + information: Information::empty(), + }); + return Ok(vec![SvcMessage::from(res)]); + } + // Return afterwards + // This can be unified with the condition for opening the file. + } else if req_inner.create_options.bits() & CreateOptions::FILE_DIRECTORY_FILE.bits() != 0 { + warn!("Attempt to create a directory, but it is a file"); + let io_response = DeviceIoResponse::new(req_inner.device_io_request, NtStatus::NOT_A_DIRECTORY); + let res = RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse { + device_io_reply: io_response, + file_id, + information: Information::empty(), + }); + return Ok(vec![SvcMessage::from(res)]); + } + } + Err(_) => { + if req_inner.create_options.bits() & CreateOptions::FILE_DIRECTORY_FILE.bits() != 0 { + if (req_inner.create_disposition == CreateDisposition::FILE_CREATE + || req_inner.create_disposition == CreateDisposition::FILE_OPEN_IF) + && std::fs::create_dir_all(path.as_str()).is_ok() + { + let mut fs = std::fs::OpenOptions::new(); + match fs.read(true).open(&path) { + Ok(file) => { + debug!("create drive file_id:{},path:{}", file_id, path); + backend.file_map.insert(file_id, file); + backend.file_path_map.insert(file_id, path.clone()); + return make_create_drive_resp( + req_inner.device_io_request, + req_inner.create_disposition, + file_id, + ); + } + Err(error) => { + warn!(%error, "Open file dir error"); + //return by downside + } + } + } + //create disposition is not correct + let io_response = DeviceIoResponse::new(req_inner.device_io_request, NtStatus::UNSUCCESSFUL); + let res = RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse { + device_io_reply: io_response, + file_id, + information: Information::empty(), + }); + return Ok(vec![SvcMessage::from(res)]); + } + } + } + + let mut fs = std::fs::OpenOptions::new(); + if CreateDisposition::FILE_OPEN_IF == req_inner.create_disposition { + fs.create(true).write(true).read(true); + } + if CreateDisposition::FILE_CREATE == req_inner.create_disposition { + fs.create_new(true).write(true).read(true); + } + if CreateDisposition::FILE_SUPERSEDE == req_inner.create_disposition { + fs.create(true).write(true).append(true).read(true); + } + if CreateDisposition::FILE_OPEN == req_inner.create_disposition { + fs.read(true); + } + if CreateDisposition::FILE_OVERWRITE == req_inner.create_disposition { + fs.write(true).truncate(true).read(true); + } + if CreateDisposition::FILE_OVERWRITE_IF == req_inner.create_disposition { + fs.write(true).truncate(true).create(true).read(true); + } + + match fs.open(&path) { + Ok(file) => { + debug!("create drive file_id:{},path:{}", file_id, path); + backend.file_map.insert(file_id, file); + backend.file_path_map.insert(file_id, path.clone()); + make_create_drive_resp(req_inner.device_io_request, req_inner.create_disposition, file_id) + } + Err(error) => { + warn!(?error, "Open file error for path:{}", path); + let io_response = DeviceIoResponse::new(req_inner.device_io_request, NtStatus::UNSUCCESSFUL); + let res = RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse { + device_io_reply: io_response, + file_id, + information: Information::empty(), + }); + Ok(vec![SvcMessage::from(res)]) + } + } +} + +pub(crate) fn process_dependent_file( + backend: &mut NixRdpdrBackend, + request: DeviceIoRequest, + error_fx: impl Fn(DeviceIoRequest) -> PduResult>, + fx: impl Fn(&mut std::fs::File, DeviceIoRequest) -> PduResult>, +) -> PduResult> { + match backend.file_map.get_mut(&request.file_id) { + None => error_fx(request), + Some(file) => fx(file, request), + } +} diff --git a/crates/ironrdp-rdpdr-native/src/nix/mod.rs b/crates/ironrdp-rdpdr-native/src/nix/mod.rs new file mode 100644 index 00000000..fceb1419 --- /dev/null +++ b/crates/ironrdp-rdpdr-native/src/nix/mod.rs @@ -0,0 +1 @@ +pub mod backend; diff --git a/crates/ironrdp-rdpdr/CHANGELOG.md b/crates/ironrdp-rdpdr/CHANGELOG.md new file mode 100644 index 00000000..e61a8251 --- /dev/null +++ b/crates/ironrdp-rdpdr/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-v0.4.1...ironrdp-rdpdr-v0.5.0)] - 2025-12-18 + +### Bug Fixes + +- Fix incorrect padding when parsing NDR strings ([#1015](https://github.com/Devolutions/IronRDP/issues/1015)) ([a0a3e750c9](https://github.com/Devolutions/IronRDP/commit/a0a3e750c9e4ee9c73b957fbcb26dbc59e57d07d)) + + When parsing Network Data Representation (NDR) messages, we're supposed + to account for padding at the end of strings to remain aligned on a + 4-byte boundary. The existing code doesn't seem to cover all cases, and + the resulting misalignment causes misleading errors when processing the + rest of the message. + +## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-v0.4.0...ironrdp-rdpdr-v0.4.1)] - 2025-09-04 + +### Features + +- Support device removal (#947) ([50574c570f](https://github.com/Devolutions/IronRDP/commit/50574c570f6e44d264153337e5f87a5313f190e6)) + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-v0.2.0...ironrdp-rdpdr-v0.3.0)] - 2025-05-27 + +### Features + +- Add USER_LOGGEDON flag support ([5e78f91713](https://github.com/Devolutions/IronRDP/commit/5e78f917132a174bdd5d8711beb1744de1bd265a)) + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-v0.1.3...ironrdp-rdpdr-v0.2.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-v0.1.2...ironrdp-rdpdr-v0.1.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-v0.1.1...ironrdp-rdpdr-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-v0.1.0...ironrdp-rdpdr-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-rdpdr/Cargo.toml b/crates/ironrdp-rdpdr/Cargo.toml new file mode 100644 index 00000000..b585bff1 --- /dev/null +++ b/crates/ironrdp-rdpdr/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ironrdp-rdpdr" +version = "0.5.0" +readme = "README.md" +description = "RDPDR channel implementation." +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public +ironrdp-error = { path = "../ironrdp-error", version = "0.1" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +tracing = { version = "0.1", features = ["log"] } +bitflags = "2.9" + +[lints] +workspace = true diff --git a/crates/ironrdp-rdpdr/LICENSE-APACHE b/crates/ironrdp-rdpdr/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-rdpdr/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-rdpdr/LICENSE-MIT b/crates/ironrdp-rdpdr/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-rdpdr/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-rdpdr/README.md b/crates/ironrdp-rdpdr/README.md new file mode 100644 index 00000000..7f18a040 --- /dev/null +++ b/crates/ironrdp-rdpdr/README.md @@ -0,0 +1,10 @@ +# IronRDP RDPDR + +Implements the RDPDR static virtual channel as described in +[\[MS-RDPEFS\]: Remote Desktop Protocol: File System Virtual Channel Extension][spec] + +[spec]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/34d9de58-b2b5-40b6-b970-f82d4603bdb5 + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-rdpdr/src/backend/mod.rs b/crates/ironrdp-rdpdr/src/backend/mod.rs new file mode 100644 index 00000000..6c0f1519 --- /dev/null +++ b/crates/ironrdp-rdpdr/src/backend/mod.rs @@ -0,0 +1,17 @@ +pub mod noop; + +use core::fmt; + +use ironrdp_core::AsAny; +use ironrdp_pdu::PduResult; +use ironrdp_svc::SvcMessage; + +use crate::pdu::efs::{DeviceControlRequest, ServerDeviceAnnounceResponse, ServerDriveIoRequest}; +use crate::pdu::esc::{ScardCall, ScardIoCtlCode}; + +/// OS-specific device redirection backend interface. +pub trait RdpdrBackend: AsAny + fmt::Debug + Send { + fn handle_server_device_announce_response(&mut self, pdu: ServerDeviceAnnounceResponse) -> PduResult<()>; + fn handle_scard_call(&mut self, req: DeviceControlRequest, call: ScardCall) -> PduResult<()>; + fn handle_drive_io_request(&mut self, req: ServerDriveIoRequest) -> PduResult>; +} diff --git a/crates/ironrdp-rdpdr/src/backend/noop.rs b/crates/ironrdp-rdpdr/src/backend/noop.rs new file mode 100644 index 00000000..82776332 --- /dev/null +++ b/crates/ironrdp-rdpdr/src/backend/noop.rs @@ -0,0 +1,24 @@ +use ironrdp_core::impl_as_any; +use ironrdp_pdu::PduResult; +use ironrdp_svc::SvcMessage; + +use super::RdpdrBackend; +use crate::pdu::efs::{DeviceControlRequest, ServerDeviceAnnounceResponse}; +use crate::pdu::esc::{ScardCall, ScardIoCtlCode}; + +#[derive(Debug)] +pub struct NoopRdpdrBackend; + +impl_as_any!(NoopRdpdrBackend); + +impl RdpdrBackend for NoopRdpdrBackend { + fn handle_server_device_announce_response(&mut self, _pdu: ServerDeviceAnnounceResponse) -> PduResult<()> { + Ok(()) + } + fn handle_scard_call(&mut self, _req: DeviceControlRequest, _call: ScardCall) -> PduResult<()> { + Ok(()) + } + fn handle_drive_io_request(&mut self, _req: crate::pdu::efs::ServerDriveIoRequest) -> PduResult> { + Ok(Vec::new()) + } +} diff --git a/crates/ironrdp-rdpdr/src/lib.rs b/crates/ironrdp-rdpdr/src/lib.rs new file mode 100644 index 00000000..e5e1191f --- /dev/null +++ b/crates/ironrdp-rdpdr/src/lib.rs @@ -0,0 +1,232 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![allow(clippy::arithmetic_side_effects)] // FIXME: remove + +use ironrdp_core::{decode_cursor, impl_as_any, ReadCursor}; +use ironrdp_pdu::gcc::ChannelName; +use ironrdp_pdu::{decode_err, pdu_other_err, PduResult}; +use ironrdp_svc::{CompressionCondition, SvcClientProcessor, SvcMessage, SvcProcessor}; +use pdu::efs::{ + Capabilities, ClientDeviceListAnnounce, ClientDeviceListRemove, ClientNameRequest, ClientNameRequestUnicodeFlag, + CoreCapability, CoreCapabilityKind, DeviceControlRequest, DeviceIoRequest, DeviceType, Devices, + ServerDeviceAnnounceResponse, VersionAndIdPdu, VersionAndIdPduKind, +}; +use pdu::esc::{ScardCall, ScardIoCtlCode}; +use pdu::RdpdrPdu; +use tracing::{debug, trace, warn}; + +pub mod backend; +pub mod pdu; + +pub use self::backend::noop::NoopRdpdrBackend; +pub use self::backend::RdpdrBackend; +use crate::pdu::efs::ServerDriveIoRequest; + +/// The RDPDR channel as specified in [\[MS-RDPEFS\]]. +/// +/// This channel must always be advertised with the "rdpsnd" +/// channel in order for the server to send anything back to it, +/// see: [\[MS-RDPEFS\] Appendix A<1>] +/// +/// [\[MS-RDPEFS\]]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/34d9de58-b2b5-40b6-b970-f82d4603bdb5 +/// [\[MS-RDPEFS\] Appendix A<1>]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/fd28bfd9-dae2-4a78-abe1-b4efa208b7aa#Appendix_A_1 +#[derive(Debug)] +pub struct Rdpdr { + /// The name of the computer that is running the client. + /// + /// Any directories shared will be displayed by File Explorer + /// as "`` on ``". + computer_name: String, + capabilities: Capabilities, + /// Pre-configured list of devices to announce to the server. + /// + /// All devices not of the type [`DeviceType::Filesystem`] must be declared here. + device_list: Devices, + backend: Box, +} + +impl_as_any!(Rdpdr); + +impl Rdpdr { + pub const NAME: ChannelName = ChannelName::from_static(b"rdpdr\0\0\0"); + + /// Creates a new [`Rdpdr`]. + pub fn new(backend: Box, computer_name: String) -> Self { + Self { + computer_name, + capabilities: Capabilities::new(), + device_list: Devices::new(), + backend, + } + } + + #[must_use] + pub fn with_smartcard(mut self, device_id: u32) -> Self { + self.capabilities.add_smartcard(); + self.device_list.add_smartcard(device_id); + self + } + + /// Adds drive redirection capability. + /// + /// Callers may also include `initial_drives` to pre-configure the list of drives to announce to the server. + /// Note that drives do not need to be pre-configured in order to be redirected, a new drive can be announced + /// at any time during a session by calling [`Self::add_drive`]. + #[must_use] + pub fn with_drives(mut self, initial_drives: Option>) -> Self { + self.capabilities.add_drive(); + if let Some(initial_drives) = initial_drives { + for (device_id, path) in initial_drives { + self.device_list.add_drive(device_id, path); + } + } + self + } + + /// Users should call this method to announce a new drive to the server. It's the caller's responsibility + /// to take the returned [`ClientDeviceListAnnounce`] and send it to the server. + pub fn add_drive(&mut self, device_id: u32, name: String) -> ClientDeviceListAnnounce { + self.device_list.add_drive(device_id, name.clone()); + ClientDeviceListAnnounce::new_drive(device_id, name) + } + + pub fn remove_device(&mut self, device_id: u32) -> Option { + Some(ClientDeviceListRemove::remove_device( + self.device_list.remove_device(device_id)?, + )) + } + + pub fn downcast_backend(&self) -> Option<&T> { + self.backend.as_any().downcast_ref::() + } + + pub fn downcast_backend_mut(&mut self) -> Option<&mut T> { + self.backend.as_any_mut().downcast_mut::() + } + + fn handle_server_announce(&mut self, req: VersionAndIdPdu) -> PduResult> { + let client_announce_reply = + RdpdrPdu::VersionAndIdPdu(VersionAndIdPdu::new_client_announce_reply(req).map_err(|e| decode_err!(e))?); + trace!("sending {:?}", client_announce_reply); + + let client_name_request = RdpdrPdu::ClientNameRequest(ClientNameRequest::new( + self.computer_name.clone(), + ClientNameRequestUnicodeFlag::Unicode, + )); + trace!("sending {:?}", client_name_request); + + Ok(vec![ + SvcMessage::from(client_announce_reply), + SvcMessage::from(client_name_request), + ]) + } + + fn handle_server_capability(&mut self, _req: CoreCapability) -> PduResult> { + let res = RdpdrPdu::CoreCapability(CoreCapability::new_response(self.capabilities.clone_inner())); + trace!("sending {:?}", res); + Ok(vec![SvcMessage::from(res)]) + } + + fn handle_client_id_confirm(&mut self) -> PduResult> { + let res = RdpdrPdu::ClientDeviceListAnnounce(ClientDeviceListAnnounce { + device_list: self.device_list.clone_inner(), + }); + trace!("sending {:?}", res); + Ok(vec![SvcMessage::from(res)]) + } + + fn handle_server_device_announce_response( + &mut self, + pdu: ServerDeviceAnnounceResponse, + ) -> PduResult> { + self.backend.handle_server_device_announce_response(pdu)?; + Ok(Vec::new()) + } + + fn handle_device_io_request( + &mut self, + dev_io_req: DeviceIoRequest, + src: &mut ReadCursor<'_>, + ) -> PduResult> { + match self + .device_list + .for_device_type(dev_io_req.device_id) + .map_err(|e| decode_err!(e))? + { + DeviceType::Smartcard => { + let req = + DeviceControlRequest::::decode(dev_io_req, src).map_err(|e| decode_err!(e))?; + let call = ScardCall::decode(req.io_control_code, src).map_err(|e| decode_err!(e))?; + + debug!(?req); + debug!(?req.io_control_code, ?call); + + self.backend.handle_scard_call(req, call)?; + + Ok(Vec::new()) + } + DeviceType::Filesystem => { + let req = ServerDriveIoRequest::decode(dev_io_req, src).map_err(|e| decode_err!(e))?; + + debug!(?req); + + Ok(self.backend.handle_drive_io_request(req)?) + } + _ => { + // This should never happen, as we only announce devices that we support. + warn!(?dev_io_req, "received packet for unsupported device type"); + Ok(Vec::new()) + } + } + } +} + +impl SvcProcessor for Rdpdr { + fn channel_name(&self) -> ChannelName { + Self::NAME + } + + fn compression_condition(&self) -> CompressionCondition { + CompressionCondition::WhenRdpDataIsCompressed + } + + fn process(&mut self, payload: &[u8]) -> PduResult> { + let mut src = ReadCursor::new(payload); + let pdu = decode_cursor::(&mut src).map_err(|e| decode_err!(e))?; + debug!("Received {:?}", pdu); + + match pdu { + RdpdrPdu::VersionAndIdPdu(pdu) if pdu.kind == VersionAndIdPduKind::ServerAnnounceRequest => { + self.handle_server_announce(pdu) + } + RdpdrPdu::CoreCapability(pdu) if pdu.kind == CoreCapabilityKind::ServerCoreCapabilityRequest => { + self.handle_server_capability(pdu) + } + RdpdrPdu::VersionAndIdPdu(pdu) if pdu.kind == VersionAndIdPduKind::ServerClientIdConfirm => { + self.handle_client_id_confirm() + } + RdpdrPdu::ServerDeviceAnnounceResponse(pdu) => self.handle_server_device_announce_response(pdu), + RdpdrPdu::DeviceIoRequest(pdu) => self.handle_device_io_request(pdu, &mut src), + RdpdrPdu::UserLoggedon => Ok(vec![]), + // TODO: This can eventually become a `_ => {}` block, but being explicit for now + // to make sure we don't miss handling new RdpdrPdu variants here during active development. + RdpdrPdu::ClientNameRequest(_) + | RdpdrPdu::ClientDeviceListAnnounce(_) + | RdpdrPdu::ClientDeviceListRemove(_) + | RdpdrPdu::VersionAndIdPdu(_) + | RdpdrPdu::CoreCapability(_) + | RdpdrPdu::DeviceControlResponse(_) + | RdpdrPdu::DeviceCreateResponse(_) + | RdpdrPdu::ClientDriveQueryInformationResponse(_) + | RdpdrPdu::DeviceCloseResponse(_) + | RdpdrPdu::ClientDriveQueryDirectoryResponse(_) + | RdpdrPdu::ClientDriveQueryVolumeInformationResponse(_) + | RdpdrPdu::DeviceReadResponse(_) + | RdpdrPdu::DeviceWriteResponse(_) + | RdpdrPdu::ClientDriveSetInformationResponse(_) + | RdpdrPdu::EmptyResponse => Err(pdu_other_err!("Rdpdr", "received unexpected packet")), + } + } +} + +impl SvcClientProcessor for Rdpdr {} diff --git a/crates/ironrdp-rdpdr/src/pdu/efs.rs b/crates/ironrdp-rdpdr/src/pdu/efs.rs new file mode 100644 index 00000000..5c669f29 --- /dev/null +++ b/crates/ironrdp-rdpdr/src/pdu/efs.rs @@ -0,0 +1,3492 @@ +//! PDUs for [\[MS-RDPEFS\]: Remote Desktop Protocol: File System Virtual Channel Extension] +//! +//! [\[MS-RDPEFS\]: Remote Desktop Protocol: File System Virtual Channel Extension]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/34d9de58-b2b5-40b6-b970-f82d4603bdb5 + +use core::fmt; +use core::fmt::{Debug, Display}; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, invalid_field_err_with_source, + unsupported_value_err, DecodeError, DecodeResult, EncodeResult, ReadCursor, WriteCursor, +}; +use ironrdp_pdu::utils::{decode_string, encoded_str_len, from_utf16_bytes, write_string_to_cursor, CharacterSet}; +use ironrdp_pdu::{read_padding, write_padding, PduError}; +use tracing::error; + +use super::esc::rpce; +use super::{PacketId, SharedHeader}; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum VersionAndIdPduKind { + /// [2.2.2.2] Server Announce Request (DR_CORE_SERVER_ANNOUNCE_REQ) + /// + /// [2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/046047aa-62d8-49f9-bf16-7fe41880aaf4 + ServerAnnounceRequest, + /// [2.2.2.3] Client Announce Reply (DR_CORE_CLIENT_ANNOUNCE_RSP) + /// + /// [2.2.2.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/d6fe6d1b-c145-4a6f-99aa-4fe3cdcea398 + ClientAnnounceReply, + /// [2.2.2.6] Server Client ID Confirm (DR_CORE_SERVER_CLIENTID_CONFIRM) + /// + /// [2.2.2.6]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/bbbb9666-6994-4cf6-8e65-0d46eb319c6e + ServerClientIdConfirm, +} + +impl VersionAndIdPduKind { + fn name(&self) -> &'static str { + match self { + VersionAndIdPduKind::ServerAnnounceRequest => "DR_CORE_SERVER_ANNOUNCE_REQ", + VersionAndIdPduKind::ClientAnnounceReply => "DR_CORE_CLIENT_ANNOUNCE_RSP", + VersionAndIdPduKind::ServerClientIdConfirm => "DR_CORE_SERVER_CLIENTID_CONFIRM", + } + } +} + +/// VersionAndIdPDU is a fixed size structure representing multiple PDUs. +/// +/// The kind field is used to determine the actual PDU type, see [`VersionAndIdPduKind`]. +#[derive(Debug, PartialEq, Clone)] +pub struct VersionAndIdPdu { + /// This field MUST be set to 0x0001 ([`VERSION_MAJOR`]). + pub version_major: u16, + pub version_minor: u16, + pub client_id: u32, + pub kind: VersionAndIdPduKind, +} + +impl VersionAndIdPdu { + const FIXED_PART_SIZE: usize = (size_of::() * 2) + size_of::(); + + pub fn new_client_announce_reply(req: VersionAndIdPdu) -> DecodeResult { + if req.kind != VersionAndIdPduKind::ServerAnnounceRequest { + return Err(invalid_field_err!( + "VersionAndIdPdu::new_client_announce_reply", + "VersionAndIdPduKind", + "invalid value" + )); + } + + Ok(Self { + version_major: VERSION_MAJOR, + version_minor: VERSION_MINOR_12, + client_id: req.client_id, + kind: VersionAndIdPduKind::ClientAnnounceReply, + }) + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(ctx: self.name(), in: dst, size: Self::FIXED_PART_SIZE); + dst.write_u16(self.version_major); + dst.write_u16(self.version_minor); + dst.write_u32(self.client_id); + Ok(()) + } + + pub fn decode(header: SharedHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + let kind = match header.packet_id { + PacketId::CoreServerAnnounce => VersionAndIdPduKind::ServerAnnounceRequest, + PacketId::CoreClientidConfirm => VersionAndIdPduKind::ServerClientIdConfirm, + _ => { + return Err(invalid_field_err!( + "VersionAndIdPdu::decode", + "PacketId", + "invalid value" + )) + } + }; + + ensure_size!(ctx: kind.name(), in: src, size: Self::FIXED_PART_SIZE); + let version_major = src.read_u16(); + let version_minor = src.read_u16(); + let client_id = src.read_u32(); + + Ok(Self { + version_major, + version_minor, + client_id, + kind, + }) + } + + pub fn name(&self) -> &'static str { + self.kind.name() + } + + pub fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +/// [2.2.2.4] Client Name Request (DR_CORE_CLIENT_NAME_REQ) +/// +/// [2.2.2.4]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/902497f1-3b1c-4aee-95f8-1668f9b7b7d2 +#[derive(Debug, PartialEq, Clone)] +pub enum ClientNameRequest { + Ascii(String), + Unicode(String), +} + +impl ClientNameRequest { + const NAME: &'static str = "DR_CORE_CLIENT_NAME_REQ"; + const FIXED_PART_SIZE: usize = size_of::() * 3; // unicode_flag + CodePage + ComputerNameLen + + pub fn new(computer_name: String, kind: ClientNameRequestUnicodeFlag) -> Self { + match kind { + ClientNameRequestUnicodeFlag::Ascii => ClientNameRequest::Ascii(computer_name), + ClientNameRequestUnicodeFlag::Unicode => ClientNameRequest::Unicode(computer_name), + } + } + + fn unicode_flag(&self) -> ClientNameRequestUnicodeFlag { + match self { + ClientNameRequest::Ascii(_) => ClientNameRequestUnicodeFlag::Ascii, + ClientNameRequest::Unicode(_) => ClientNameRequestUnicodeFlag::Unicode, + } + } + + fn computer_name(&self) -> &str { + match self { + ClientNameRequest::Ascii(name) => name, + ClientNameRequest::Unicode(name) => name, + } + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let encoded_computer_name_length = cast_length!( + "encoded computer name length", + encoded_str_len(self.computer_name(), self.unicode_flag().into(), true) + )?; + + dst.write_u32(self.unicode_flag().into()); + dst.write_u32(0); // // CodePage (4 bytes): it MUST be set to 0 + dst.write_u32(encoded_computer_name_length); + write_string_to_cursor(dst, self.computer_name(), self.unicode_flag().into(), true) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + Self::FIXED_PART_SIZE + encoded_str_len(self.computer_name(), self.unicode_flag().into(), true) + } +} + +#[repr(u32)] +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ClientNameRequestUnicodeFlag { + Ascii = 0x0, + Unicode = 0x1, +} + +impl From for CharacterSet { + fn from(val: ClientNameRequestUnicodeFlag) -> Self { + match val { + ClientNameRequestUnicodeFlag::Ascii => CharacterSet::Ansi, + ClientNameRequestUnicodeFlag::Unicode => CharacterSet::Unicode, + } + } +} + +impl From for u32 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(val: ClientNameRequestUnicodeFlag) -> Self { + val as u32 + } +} + +/// [2.2.2.7] Server Core Capability Request (DR_CORE_CAPABILITY_REQ) +/// and [2.2.2.8] Client Core Capability Response (DR_CORE_CAPABILITY_RSP) +/// +/// [2.2.2.7]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/702789c3-b924-4bc2-9280-3221bc7d6797 +/// [2.2.2.8]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/f513bf87-cca0-488a-ac5c-18cf18f4a7e1 +#[derive(Debug, PartialEq, Clone)] +pub struct CoreCapability { + pub capabilities: Vec, + pub kind: CoreCapabilityKind, +} + +impl CoreCapability { + const FIXED_PART_SIZE: usize = size_of::() * 2; + + /// Creates a new [`DR_CORE_CAPABILITY_RSP`] with the given `capabilities`. + /// + /// [`DR_CORE_CAPABILITY_RSP`]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/f513bf87-cca0-488a-ac5c-18cf18f4a7e1 + pub fn new_response(capabilities: Vec) -> Self { + Self { + capabilities, + kind: CoreCapabilityKind::ClientCoreCapabilityResponse, + } + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(ctx: self.name(), in: dst, size: self.size()); + dst.write_u16(cast_length!( + "CoreCapability", + "numCapabilities", + self.capabilities.len() + )?); + write_padding!(dst, 2); // 2-bytes padding + for cap in self.capabilities.iter() { + cap.encode(dst)?; + } + Ok(()) + } + + pub fn decode(header: SharedHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + let kind = match header.packet_id { + PacketId::CoreServerCapability => CoreCapabilityKind::ServerCoreCapabilityRequest, + PacketId::CoreClientCapability => CoreCapabilityKind::ClientCoreCapabilityResponse, + _ => { + return Err(invalid_field_err!( + "CoreCapability::decode", + "PacketId", + "invalid value" + )) + } + }; + + ensure_size!(ctx: kind.name(), in: src, size: Self::FIXED_PART_SIZE); + + let num_capabilities = src.read_u16(); + read_padding!(src, 2); // 2-bytes padding + let mut capabilities = Vec::new(); + for _ in 0..num_capabilities { + capabilities.push(CapabilityMessage::decode(src)?); + } + + Ok(Self { capabilities, kind }) + } + + pub fn name(&self) -> &'static str { + self.kind.name() + } + + pub fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.capabilities.iter().map(|c| c.size()).sum::() + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum CoreCapabilityKind { + /// [2.2.2.7] Server Core Capability Request (DR_CORE_CAPABILITY_REQ) + /// + /// [2.2.2.7]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/702789c3-b924-4bc2-9280-3221bc7d6797 + ServerCoreCapabilityRequest, + /// [2.2.2.8] Client Core Capability Response (DR_CORE_CAPABILITY_RSP) + /// + /// [2.2.2.8]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/f513bf87-cca0-488a-ac5c-18cf18f4a7e1 + ClientCoreCapabilityResponse, +} + +impl CoreCapabilityKind { + fn name(&self) -> &'static str { + match self { + CoreCapabilityKind::ServerCoreCapabilityRequest => "DR_CORE_CAPABILITY_REQ", + CoreCapabilityKind::ClientCoreCapabilityResponse => "DR_CORE_CAPABILITY_RSP", + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Capabilities(Vec); + +impl Capabilities { + pub fn new() -> Self { + let mut this = Self(Vec::new()); + this.add_general(0); + this + } + + pub fn clone_inner(&mut self) -> Vec { + self.0.clone() + } + + pub fn add_smartcard(&mut self) { + self.push(CapabilityMessage::new_smartcard()); + self.increment_special_devices(); + } + + pub fn add_drive(&mut self) { + self.push(CapabilityMessage::new_drive()); + } + + fn add_general(&mut self, special_type_device_cap: u32) { + self.push(CapabilityMessage::new_general(special_type_device_cap)); + } + + fn push(&mut self, capability: CapabilityMessage) { + self.0.push(capability); + } + + fn increment_special_devices(&mut self) { + let capabilities = &mut self.0; + for capability in capabilities.iter_mut() { + match &mut capability.capability_data { + CapabilityData::General(general_capability) => { + general_capability.special_type_device_cap += 1; + break; + } + _ => continue, + } + } + } +} + +impl Default for Capabilities { + fn default() -> Self { + Self::new() + } +} + +/// [2.2.1.2.1] Capability Message (CAPABILITY_SET) +/// +/// [2.2.1.2.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/f1b9dd1d-2c37-4aac-9836-4b0df02369ba +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct CapabilityMessage { + header: CapabilityHeader, + capability_data: CapabilityData, +} + +impl CapabilityMessage { + /// Creates a new [`GENERAL_CAPS_SET`]. + /// + /// `special_type_device_cap`: A 32-bit unsigned integer that + /// specifies the number of special devices to be redirected + /// before the user is logged on. Special devices are those + /// that are safe and/or required to be redirected before a + /// user logs on (such as smart cards and serial ports). + /// + /// [`GENERAL_CAPS_SET`]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/06c7cb30-303d-4fa2-b396-806df8ac1501 + pub fn new_general(special_type_device_cap: u32) -> Self { + Self { + header: CapabilityHeader::new_general(), + capability_data: CapabilityData::General(GeneralCapabilitySet { + os_type: 0, + os_version: 0, + protocol_major_version: 1, + protocol_minor_version: VERSION_MINOR_12, + io_code_1: IoCode1::REQUIRED, + io_code_2: 0, + extended_pdu: ExtendedPdu::RDPDR_DEVICE_REMOVE_PDUS + | ExtendedPdu::RDPDR_CLIENT_DISPLAY_NAME_PDU + | ExtendedPdu::RDPDR_USER_LOGGEDON_PDU, + extra_flags_1: ExtraFlags1::empty(), + extra_flags_2: 0, + special_type_device_cap, + }), + } + } + + /// Creates a new [`SMARTCARD_CAPS_SET`]. + /// + /// [`SMARTCARD_CAPS_SET`]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e02de60a-4d32-4dc7-ab17-9d591129eb93 + pub fn new_smartcard() -> Self { + Self { + header: CapabilityHeader::new_smartcard(), + capability_data: CapabilityData::Smartcard, + } + } + + /// Creates a new [`DRIVE_CAPS_SET`]. + /// + /// [`DRIVE_CAPS_SET`]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/4f018cd2-60ba-4c7b-adcf-55bd05cea6f8 + pub fn new_drive() -> Self { + Self { + header: CapabilityHeader::new_drive(), + capability_data: CapabilityData::Drive, + } + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.header.encode(dst)?; + self.capability_data.encode(dst) + } + + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + let header = CapabilityHeader::decode(src)?; + let capability_data = CapabilityData::decode(src, &header)?; + + Ok(Self { + header, + capability_data, + }) + } + + fn size(&self) -> usize { + CapabilityHeader::SIZE + self.capability_data.size() + } +} + +/// [2.2.1.2] Capability Header (CAPABILITY_HEADER) +/// +/// [2.2.1.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/b3c3304a-2e1b-4667-97e9-3bce49544907 +#[derive(Debug, PartialEq, Clone, Copy)] +struct CapabilityHeader { + cap_type: CapabilityType, + length: u16, + version: u32, +} + +impl CapabilityHeader { + const SIZE: usize = size_of::() * 2 + size_of::(); + + fn new_general() -> Self { + Self { + cap_type: CapabilityType::General, + length: u16::try_from(Self::SIZE + GeneralCapabilitySet::SIZE).expect("value fits into u16"), + version: GENERAL_CAPABILITY_VERSION_02, + } + } + + fn new_smartcard() -> Self { + Self { + cap_type: CapabilityType::Smartcard, + length: u16::try_from(Self::SIZE).expect("value fits into u16"), + version: SMARTCARD_CAPABILITY_VERSION_01, + } + } + + fn new_drive() -> Self { + Self { + cap_type: CapabilityType::Drive, + length: u16::try_from(Self::SIZE).expect("value fits into u16"), + version: DRIVE_CAPABILITY_VERSION_02, + } + } + + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::SIZE); + let cap_type: CapabilityType = src.read_u16().try_into()?; + let length = src.read_u16(); + let version = src.read_u32(); + + Ok(Self { + cap_type, + length, + version, + }) + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::SIZE); + dst.write_u16(self.cap_type.into()); + dst.write_u16(self.length); + dst.write_u32(self.version); + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Copy)] +#[repr(u16)] +enum CapabilityType { + /// CAP_GENERAL_TYPE + General = 0x0001, + /// CAP_PRINTER_TYPE + Printer = 0x0002, + /// CAP_PORT_TYPE + Port = 0x0003, + /// CAP_DRIVE_TYPE + Drive = 0x0004, + /// CAP_SMARTCARD_TYPE + Smartcard = 0x0005, +} + +impl From for u16 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(cap_type: CapabilityType) -> Self { + cap_type as u16 + } +} + +/// GENERAL_CAPABILITY_VERSION_02 +pub const GENERAL_CAPABILITY_VERSION_02: u32 = 0x0000_0002; +/// SMARTCARD_CAPABILITY_VERSION_01 +pub const SMARTCARD_CAPABILITY_VERSION_01: u32 = 0x0000_0001; +/// DRIVE_CAPABILITY_VERSION_02 +pub const DRIVE_CAPABILITY_VERSION_02: u32 = 0x0000_0002; + +impl TryFrom for CapabilityType { + type Error = DecodeError; + + fn try_from(value: u16) -> Result { + match value { + 0x0001 => Ok(CapabilityType::General), + 0x0002 => Ok(CapabilityType::Printer), + 0x0003 => Ok(CapabilityType::Port), + 0x0004 => Ok(CapabilityType::Drive), + 0x0005 => Ok(CapabilityType::Smartcard), + _ => Err(invalid_field_err!("try_from", "CapabilityType", "invalid value")), + } + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +enum CapabilityData { + General(GeneralCapabilitySet), + Printer, + Port, + Drive, + Smartcard, +} + +impl CapabilityData { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + CapabilityData::General(general) => general.encode(dst), + _ => Ok(()), + } + } + + fn decode(src: &mut ReadCursor<'_>, header: &CapabilityHeader) -> DecodeResult { + match header.cap_type { + CapabilityType::General => Ok(CapabilityData::General(GeneralCapabilitySet::decode( + src, + header.version, + )?)), + CapabilityType::Printer => Ok(CapabilityData::Printer), + CapabilityType::Port => Ok(CapabilityData::Port), + CapabilityType::Drive => Ok(CapabilityData::Drive), + CapabilityType::Smartcard => Ok(CapabilityData::Smartcard), + } + } + + fn size(&self) -> usize { + match self { + CapabilityData::General(_) => GeneralCapabilitySet::SIZE, + CapabilityData::Printer => 0, + CapabilityData::Port => 0, + CapabilityData::Drive => 0, + CapabilityData::Smartcard => 0, + } + } +} + +/// [2.2.2.7.1] General Capability Set (GENERAL_CAPS_SET) +/// +/// [2.2.2.7.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/06c7cb30-303d-4fa2-b396-806df8ac1501 +#[derive(Debug, PartialEq, Clone, Copy)] +struct GeneralCapabilitySet { + /// MUST be ignored. + os_type: u32, + /// SHOULD be ignored. + os_version: u32, + /// MUST be set to 1. + protocol_major_version: u16, + /// MUST be set to one of the values described by the VersionMinor field + /// of the [Server Client ID Confirm (section 2.2.2.6)] packet. + /// + /// [Server Client ID Confirm (section 2.2.2.6)]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/bbbb9666-6994-4cf6-8e65-0d46eb319c6e + protocol_minor_version: u16, + /// See [`IoCode1`]. + io_code_1: IoCode1, + /// MUST be set to 0. + io_code_2: u32, + /// See [`ExtendedPdu`]. + extended_pdu: ExtendedPdu, + /// See [`ExtraFlags1`]. + extra_flags_1: ExtraFlags1, + /// MUST be set to 0. + extra_flags_2: u32, + /// A 32-bit unsigned integer that specifies the number + /// of special devices to be redirected before the user + /// is logged on. Special devices are those that are safe + /// and/or required to be redirected before a user logs + /// on (such as smart cards and serial ports). + special_type_device_cap: u32, +} + +impl GeneralCapabilitySet { + #[expect(clippy::manual_bits)] + const SIZE: usize = size_of::() * 8 + size_of::() * 2; + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::SIZE); + dst.write_u32(self.os_type); + dst.write_u32(self.os_version); + dst.write_u16(self.protocol_major_version); + dst.write_u16(self.protocol_minor_version); + dst.write_u32(self.io_code_1.bits()); + dst.write_u32(self.io_code_2); + dst.write_u32(self.extended_pdu.bits()); + dst.write_u32(self.extra_flags_1.bits()); + dst.write_u32(self.extra_flags_2); + dst.write_u32(self.special_type_device_cap); + Ok(()) + } + + fn decode(src: &mut ReadCursor<'_>, version: u32) -> DecodeResult { + ensure_size!(in: src, size: Self::SIZE); + let os_type = src.read_u32(); + let os_version = src.read_u32(); + let protocol_major_version = src.read_u16(); + let protocol_minor_version = src.read_u16(); + let io_code_1 = IoCode1::from_bits_retain(src.read_u32()); + let io_code_2 = src.read_u32(); + let extended_pdu = ExtendedPdu::from_bits_retain(src.read_u32()); + let extra_flags_1 = ExtraFlags1::from_bits_retain(src.read_u32()); + let extra_flags_2 = src.read_u32(); + let special_type_device_cap = if version == GENERAL_CAPABILITY_VERSION_02 { + src.read_u32() + } else { + 0 + }; + + Ok(Self { + os_type, + os_version, + protocol_major_version, + protocol_minor_version, + io_code_1, + io_code_2, + extended_pdu, + extra_flags_1, + extra_flags_2, + special_type_device_cap, + }) + } +} + +bitflags! { + /// A 32-bit unsigned integer that identifies a bitmask of the supported I/O requests for the given device. + /// If the bit is set, the I/O request is allowed. The requests are identified by the MajorFunction field + /// in the Device I/O Request (section 2.2.1.4) header. This field MUST be set to a valid combination of + /// the following values. + #[derive(Debug, PartialEq, Clone, Copy)] + struct IoCode1: u32 { + /// Unused, always set. + const RDPDR_IRP_MJ_CREATE = 0x0000_0001; + /// Unused, always set. + const RDPDR_IRP_MJ_CLEANUP = 0x0000_0002; + /// Unused, always set. + const RDPDR_IRP_MJ_CLOSE = 0x0000_0004; + /// Unused, always set. + const RDPDR_IRP_MJ_READ = 0x0000_0008; + /// Unused, always set. + const RDPDR_IRP_MJ_WRITE = 0x0000_0010; + /// Unused, always set. + const RDPDR_IRP_MJ_FLUSH_BUFFERS = 0x0000_0020; + /// Unused, always set. + const RDPDR_IRP_MJ_SHUTDOWN = 0x0000_0040; + /// Unused, always set. + const RDPDR_IRP_MJ_DEVICE_CONTROL = 0x0000_0080; + /// Unused, always set. + const RDPDR_IRP_MJ_QUERY_VOLUME_INFORMATION = 0x0000_0100; + /// Unused, always set. + const RDPDR_IRP_MJ_SET_VOLUME_INFORMATION = 0x0000_0200; + /// Unused, always set. + const RDPDR_IRP_MJ_QUERY_INFORMATION = 0x0000_0400; + /// Unused, always set. + const RDPDR_IRP_MJ_SET_INFORMATION = 0x0000_0800; + /// Unused, always set. + const RDPDR_IRP_MJ_DIRECTORY_CONTROL = 0x0000_1000; + /// Unused, always set. + const RDPDR_IRP_MJ_LOCK_CONTROL = 0x0000_2000; + /// Enable Query Security requests (IRP_MJ_QUERY_SECURITY). + const RDPDR_IRP_MJ_QUERY_SECURITY = 0x0000_4000; + /// Enable Set Security requests (IRP_MJ_SET_SECURITY). + const RDPDR_IRP_MJ_SET_SECURITY = 0x0000_8000; + + /// Combination of all the required bits. + const REQUIRED = Self::RDPDR_IRP_MJ_CREATE.bits() + | Self::RDPDR_IRP_MJ_CLEANUP.bits() + | Self::RDPDR_IRP_MJ_CLOSE.bits() + | Self::RDPDR_IRP_MJ_READ.bits() + | Self::RDPDR_IRP_MJ_WRITE.bits() + | Self::RDPDR_IRP_MJ_FLUSH_BUFFERS.bits() + | Self::RDPDR_IRP_MJ_SHUTDOWN.bits() + | Self::RDPDR_IRP_MJ_DEVICE_CONTROL.bits() + | Self::RDPDR_IRP_MJ_QUERY_VOLUME_INFORMATION.bits() + | Self::RDPDR_IRP_MJ_SET_VOLUME_INFORMATION.bits() + | Self::RDPDR_IRP_MJ_QUERY_INFORMATION.bits() + | Self::RDPDR_IRP_MJ_SET_INFORMATION.bits() + | Self::RDPDR_IRP_MJ_DIRECTORY_CONTROL.bits() + | Self::RDPDR_IRP_MJ_LOCK_CONTROL.bits(); + + } +} + +bitflags! { + /// A 32-bit unsigned integer that specifies extended PDU flags. + /// This field MUST be set as a bitmask of the following values. + #[derive(Debug, PartialEq, Clone, Copy)] + struct ExtendedPdu: u32 { + /// Allow the client to send Client Drive Device List Remove packets. + const RDPDR_DEVICE_REMOVE_PDUS = 0x0000_0001; + /// Unused, always set. + const RDPDR_CLIENT_DISPLAY_NAME_PDU = 0x0000_0002; + /// Allow the server to send a Server User Logged On packet. + const RDPDR_USER_LOGGEDON_PDU = 0x0000_0004; + } +} + +bitflags! { + /// A 32-bit unsigned integer that specifies extended flags. + /// The extraFlags1 field MUST be set as a bitmask of the following value. + #[derive(Debug, PartialEq, Clone, Copy)] + struct ExtraFlags1: u32 { + /// Optionally present only in the Client Core Capability Response. + /// Allows the server to send multiple simultaneous read or write requests + /// on the same file from a redirected file system. + const ENABLE_ASYNCIO = 0x0000_0001; + } +} + +/// From VersionMinor in [Server Client ID Confirm (section 2.2.2.6)], [2.2.2.3 Client Announce Reply (DR_CORE_CLIENT_ANNOUNCE_RSP)] +/// +/// VERSION_MINOR_12 is what Teleport has successfully been using. +/// There is a version 13 as well, but it's not clear to me what +/// the difference is. +/// +/// [Server Client ID Confirm (section 2.2.2.6)]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/bbbb9666-6994-4cf6-8e65-0d46eb319c6e +/// [2.2.2.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/d6fe6d1b-c145-4a6f-99aa-4fe3cdcea398 +pub const VERSION_MINOR_12: u16 = 0x000C; +pub const VERSION_MAJOR: u16 = 0x0001; + +/// [2.2.2.9] Client Device List Announce Request (DR_CORE_DEVICELIST_ANNOUNCE_REQ) +/// and [2.2.3.1] Client Device List Announce (DR_DEVICELIST_ANNOUNCE) +/// +/// [2.2.2.9]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/10ef9ada-cba2-4384-ab60-7b6290ed4a9a +/// [2.2.3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/d8b2bc1c-0207-4c15-abe3-62eaa2afcaf1 +#[derive(Debug, PartialEq, Clone)] +pub struct ClientDeviceListAnnounce { + pub device_list: Vec, +} + +impl ClientDeviceListAnnounce { + const FIXED_PART_SIZE: usize = size_of::(); // DeviceCount + + /// Library users should not typically call this directly, use [`Rdpdr::add_drive`] instead. + pub(crate) fn new_drive(device_id: u32, name: String) -> Self { + Self { + device_list: vec![DeviceAnnounceHeader::new_drive(device_id, name)], + } + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + dst.write_u32(cast_length!( + "ClientDeviceListAnnounce", + "DeviceCount", + self.device_list.len() + )?); + + for dev in self.device_list.iter() { + dev.encode(dst)?; + } + + Ok(()) + } + + pub fn name(&self) -> &'static str { + "DR_CORE_DEVICELIST_ANNOUNCE_REQ" + } + + pub fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.device_list.iter().map(|d| d.size()).sum::() + } +} + +/// [2.2.3.2] Client Device List Remove (DR_DEVICELIST_REMOVE) +/// +/// [2.2.3.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/13bd4c0a-e674-47a5-b317-50a835defb55 +#[derive(Debug, PartialEq, Clone)] +pub struct ClientDeviceListRemove { + pub device_list: Vec, +} + +impl ClientDeviceListRemove { + const FIXED_PART_SIZE: usize = size_of::(); // DeviceCount + + pub(crate) fn remove_device(device_id: u32) -> Self { + Self { + device_list: vec![device_id], + } + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + dst.write_u32(cast_length!( + "ClientDeviceListRemove", + "DeviceCount", + self.device_list.len() + )?); + + for dev in self.device_list.iter() { + dst.write_u32(*dev) + } + + Ok(()) + } + + pub fn name(&self) -> &'static str { + "DR_DEVICELIST_REMOVE" + } + + pub fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.device_list.len() * size_of::() + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Devices(Vec); + +impl Devices { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn add_smartcard(&mut self, device_id: u32) { + self.push(DeviceAnnounceHeader::new_smartcard(device_id)); + } + + pub fn add_drive(&mut self, device_id: u32, name: String) { + self.push(DeviceAnnounceHeader::new_drive(device_id, name)); + } + + pub fn remove_device(&mut self, device_id: u32) -> Option { + self.remove(device_id) + } + + /// Returns the [`DeviceType`] for the given device ID. + pub fn for_device_type(&self, device_id: u32) -> DecodeResult { + if let Some(device_type) = self.0.iter().find(|d| d.device_id == device_id).map(|d| d.device_type) { + Ok(device_type) + } else { + Err(invalid_field_err!( + "Devices::for_device_type", + "device_id", + "no device with that ID" + )) + } + } + + fn push(&mut self, device: DeviceAnnounceHeader) { + self.0.push(device); + } + + fn remove(&mut self, device: u32) -> Option { + Some( + self.0 + .remove( + self.0 + .iter() + .position(|d: &DeviceAnnounceHeader| d.device_id == device)?, + ) + .device_id, + ) + } + + pub fn clone_inner(&mut self) -> Vec { + self.0.clone() + } +} + +impl Default for Devices { + fn default() -> Self { + Self::new() + } +} + +/// [2.2.1.3] Device Announce Header (DEVICE_ANNOUNCE) +/// +/// [2.2.1.3]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/32e34332-774b-4ead-8c9d-5d64720d6bf9 +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceAnnounceHeader { + device_type: DeviceType, + device_id: u32, + preferred_dos_name: PreferredDosName, + device_data: Vec, +} + +impl DeviceAnnounceHeader { + const FIXED_PART_SIZE: usize = size_of::() * 3 + 8; // DeviceType, DeviceId, DeviceDataLength, PreferredDosName + + pub fn new_smartcard(device_id: u32) -> Self { + Self { + device_type: DeviceType::Smartcard, + device_id, + // This name is a constant defined by the spec. + preferred_dos_name: PreferredDosName("SCARD".to_owned()), + device_data: Vec::new(), + } + } + + fn new_drive(device_id: u32, name: String) -> Self { + // The spec says Unicode but empirically this wants null terminated UTF-8. + let mut device_data = name.into_bytes(); + device_data.push(0u8); + + Self { + device_type: DeviceType::Filesystem, + device_id, + // "The drive name MUST be specified in the PreferredDosName field; however, if the drive name is larger than the allocated size of the PreferredDosName field, + // then the drive name MUST be truncated to fit. If the client supports DRIVE_CAPABILITY_VERSION_02 in the Drive Capability Set, then the full name MUST also + // be specified in the DeviceData field, as a null-terminated Unicode string. If the DeviceDataLength field is nonzero, the content of the PreferredDosName field + // is ignored." + // + // Since we do support DRIVE_CAPABILITY_VERSION_02, we'll put the full name in the DeviceData field. + preferred_dos_name: PreferredDosName("ignored".to_owned()), + device_data, + } + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + dst.write_u32(self.device_type.into()); + dst.write_u32(self.device_id); + self.preferred_dos_name.encode(dst)?; + dst.write_u32(cast_length!( + "DeviceAnnounceHeader", + "DeviceDataLength", + self.device_data.len() + )?); + dst.write_slice(&self.device_data); + Ok(()) + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.device_data.len() + } +} + +/// From ["PreferredDosName"](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/32e34332-774b-4ead-8c9d-5d64720d6bf9): +/// +/// PreferredDosName (8 bytes): A string of ASCII characters (with a maximum length of eight characters) that represents the name of the device as it appears on the client. This field MUST be null-terminated, so the maximum device name is 7 characters long. The following characters are considered invalid for the PreferredDosName field: +/// +/// <, >, ", /, \, | +/// +/// If any of these characters are present, the DR_CORE_DEVICE_ANNOUNC_RSP packet for this device (section 2.2.2.1) will be sent with STATUS_ACCESS_DENIED set in the ResultCode field. +/// +/// If DeviceType is set to RDPDR_DTYP_SMARTCARD, the PreferredDosName MUST be set to "SCARD". +/// +/// Note A column character, ":", is valid only when present at the end of the PreferredDosName field, otherwise it is also considered invalid. +#[derive(Debug, PartialEq, Clone)] +struct PreferredDosName(String); + +impl PreferredDosName { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + write_string_to_cursor(dst, &self.format(), CharacterSet::Ansi, false) + } + + /// Returns the underlying String with a maximum length of 7 characters plus a null terminator. + fn format(&self) -> String { + let mut name: &str = &self.0; + if name.len() > 7 { + name = name + .get(..7) + .expect("index is guaranteed to be on a UTF-8 boundary for a string of ASCII characters"); + } + format!("{name:\x00<8}") + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u32)] +pub enum DeviceType { + /// RDPDR_DTYP_SERIAL + Serial = 0x0000_0001, + /// RDPDR_DTYP_PARALLEL + Parallel = 0x0000_0002, + /// RDPDR_DTYP_PRINT + Print = 0x0000_0004, + /// RDPDR_DTYP_FILESYSTEM + Filesystem = 0x0000_0008, + /// RDPDR_DTYP_SMARTCARD + Smartcard = 0x0000_0020, +} + +impl From for u32 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(device_type: DeviceType) -> Self { + device_type as u32 + } +} + +impl TryFrom for DeviceType { + type Error = DecodeError; + + fn try_from(value: u32) -> Result { + match value { + 0x0000_0001 => Ok(DeviceType::Serial), + 0x0000_0002 => Ok(DeviceType::Parallel), + 0x0000_0004 => Ok(DeviceType::Print), + 0x0000_0008 => Ok(DeviceType::Filesystem), + 0x0000_0020 => Ok(DeviceType::Smartcard), + _ => Err(invalid_field_err!("try_from", "DeviceType", "invalid value")), + } + } +} + +/// [2.2.2.1] Server Device Announce Response (DR_CORE_DEVICE_ANNOUNCE_RSP) +/// +/// [2.2.2.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a4c0b619-6e87-4721-bdc4-5d2db7f485f3 +#[derive(Debug, PartialEq, Clone)] +pub struct ServerDeviceAnnounceResponse { + pub device_id: u32, + pub result_code: NtStatus, +} + +impl ServerDeviceAnnounceResponse { + const NAME: &'static str = "DR_CORE_DEVICE_ANNOUNCE_RSP"; + const FIXED_PART_SIZE: usize = size_of::() * 2; // DeviceId, ResultCode + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.device_id); + dst.write_u32(self.result_code.into()); + Ok(()) + } + + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(ctx: Self::NAME, in: src, size: Self::FIXED_PART_SIZE); + let device_id = src.read_u32(); + let result_code = NtStatus::from(src.read_u32()); + + Ok(Self { device_id, result_code }) + } + + pub fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +/// [2.3.1] NTSTATUS Values +/// +/// Windows defines an absolutely massive list of potential NTSTATUS values. +/// This enum includes some basic ones for communicating with the RDP server. +/// +/// [2.3.1]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct NtStatus(u32); + +impl NtStatus { + /// STATUS_SUCCESS + pub const SUCCESS: Self = Self(0x0000_0000); + /// STATUS_UNSUCCESSFUL + pub const UNSUCCESSFUL: Self = Self(0xC000_0001); + /// STATUS_NOT_IMPLEMENTED + pub const NOT_IMPLEMENTED: Self = Self(0xC000_0002); + /// STATUS_NO_MORE_FILES + pub const NO_MORE_FILES: Self = Self(0x8000_0006); + /// STATUS_OBJECT_NAME_COLLISION + pub const OBJECT_NAME_COLLISION: Self = Self(0xC000_0035); + /// STATUS_ACCESS_DENIED + pub const ACCESS_DENIED: Self = Self(0xC000_0022); + /// STATUS_NOT_A_DIRECTORY + pub const NOT_A_DIRECTORY: Self = Self(0xC000_0103); + /// STATUS_NO_SUCH_FILE + pub const NO_SUCH_FILE: Self = Self(0xC000_000F); + /// STATUS_NOT_SUPPORTED + pub const NOT_SUPPORTED: Self = Self(0xC000_00BB); + /// STATUS_DIRECTORY_NOT_EMPTY + pub const DIRECTORY_NOT_EMPTY: Self = Self(0xC000_0101); +} + +impl Debug for NtStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + NtStatus::SUCCESS => write!(f, "STATUS_SUCCESS"), + NtStatus::UNSUCCESSFUL => write!(f, "STATUS_UNSUCCESSFUL"), + NtStatus::NOT_IMPLEMENTED => write!(f, "STATUS_NOT_IMPLEMENTED"), + NtStatus::NO_MORE_FILES => write!(f, "STATUS_NO_MORE_FILES"), + NtStatus::OBJECT_NAME_COLLISION => write!(f, "STATUS_OBJECT_NAME_COLLISION"), + NtStatus::ACCESS_DENIED => write!(f, "STATUS_ACCESS_DENIED"), + NtStatus::NOT_A_DIRECTORY => write!(f, "STATUS_NOT_A_DIRECTORY"), + NtStatus::NO_SUCH_FILE => write!(f, "STATUS_NO_SUCH_FILE"), + NtStatus::NOT_SUPPORTED => write!(f, "STATUS_NOT_SUPPORTED"), + NtStatus::DIRECTORY_NOT_EMPTY => write!(f, "STATUS_DIRECTORY_NOT_EMPTY"), + _ => write!(f, "NtStatus({:#010X})", self.0), + } + } +} + +impl From for NtStatus { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(status: NtStatus) -> Self { + status.0 + } +} + +/// [2.2.1.4] Device I/O Request (DR_DEVICE_IOREQUEST) +/// +/// [2.2.1.4]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a087ffa8-d0d5-4874-ac7b-0494f63e2d5d +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceIoRequest { + pub device_id: u32, + pub file_id: u32, + pub completion_id: u32, + pub major_function: MajorFunction, + pub minor_function: MinorFunction, +} + +impl DeviceIoRequest { + const NAME: &'static str = "DR_DEVICE_IOREQUEST"; + const FIXED_PART_SIZE: usize = size_of::() * 5; // DeviceId, FileId, CompletionId, MajorFunction, MinorFunction + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.device_id); + dst.write_u32(self.file_id); + dst.write_u32(self.completion_id); + dst.write_u32(self.major_function.into()); + dst.write_u32(self.minor_function.into()); + Ok(()) + } + + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(ctx: Self::NAME, in: src, size: Self::FIXED_PART_SIZE); + let device_id = src.read_u32(); + let file_id = src.read_u32(); + let completion_id = src.read_u32(); + let major_function = MajorFunction::try_from(src.read_u32())?; + let minor_function = MinorFunction::from(src.read_u32()); + + Ok(Self { + device_id, + file_id, + completion_id, + major_function, + minor_function, + }) + } + + pub fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +/// See [`DeviceIoRequest`]. +#[derive(Debug, PartialEq, Clone, Copy)] +#[repr(u32)] +pub enum MajorFunction { + /// IRP_MJ_CREATE + Create = 0x0000_0000, + /// IRP_MJ_CLOSE + Close = 0x0000_0002, + /// IRP_MJ_READ + Read = 0x0000_0003, + /// IRP_MJ_WRITE + Write = 0x0000_0004, + /// IRP_MJ_DEVICE_CONTROL + DeviceControl = 0x0000_000e, + /// IRP_MJ_QUERY_VOLUME_INFORMATION + QueryVolumeInformation = 0x0000_000a, + /// IRP_MJ_SET_VOLUME_INFORMATION + SetVolumeInformation = 0x0000_000b, + /// IRP_MJ_QUERY_INFORMATION + QueryInformation = 0x0000_0005, + /// IRP_MJ_SET_INFORMATION + SetInformation = 0x0000_0006, + /// IRP_MJ_DIRECTORY_CONTROL + DirectoryControl = 0x0000_000c, + /// IRP_MJ_LOCK_CONTROL + LockControl = 0x0000_0011, +} + +impl TryFrom for MajorFunction { + type Error = DecodeError; + + fn try_from(value: u32) -> Result { + match value { + 0x0000_0000 => Ok(MajorFunction::Create), + 0x0000_0002 => Ok(MajorFunction::Close), + 0x0000_0003 => Ok(MajorFunction::Read), + 0x0000_0004 => Ok(MajorFunction::Write), + 0x0000_000e => Ok(MajorFunction::DeviceControl), + 0x0000_000a => Ok(MajorFunction::QueryVolumeInformation), + 0x0000_000b => Ok(MajorFunction::SetVolumeInformation), + 0x0000_0005 => Ok(MajorFunction::QueryInformation), + 0x0000_0006 => Ok(MajorFunction::SetInformation), + 0x0000_000c => Ok(MajorFunction::DirectoryControl), + 0x0000_0011 => Ok(MajorFunction::LockControl), + _ => Err(invalid_field_err!("try_from", "MajorFunction", "unsupported value")), + } + } +} + +impl From for u32 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(major_function: MajorFunction) -> Self { + major_function as u32 + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +/// A 32-bit unsigned integer. This field is valid only when the MajorFunction field is +/// set to IRP_MJ_DIRECTORY_CONTROL. If the MajorFunction field is set to another value, +/// the MinorFunction field value SHOULD be 0x00000000; otherwise, the MinorFunction +/// field MUST have one of the following values: +/// +/// 1. [`MinorFunction::IRP_MN_QUERY_DIRECTORY`] +/// 2. [`MinorFunction::IRP_MN_NOTIFY_CHANGE_DIRECTORY`] +pub struct MinorFunction(u32); + +impl MinorFunction { + pub const IRP_MN_QUERY_DIRECTORY: Self = Self(0x00000001); + pub const IRP_MN_NOTIFY_CHANGE_DIRECTORY: Self = Self(0x00000002); +} + +impl Debug for MinorFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + MinorFunction::IRP_MN_QUERY_DIRECTORY => write!(f, "IRP_MN_QUERY_DIRECTORY"), + MinorFunction::IRP_MN_NOTIFY_CHANGE_DIRECTORY => write!(f, "IRP_MN_NOTIFY_CHANGE_DIRECTORY"), + _ => write!(f, "MinorFunction({:#010X})", self.0), + } + } +} + +impl From for MinorFunction { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(minor_function: MinorFunction) -> Self { + minor_function.0 + } +} + +/// [2.2.1.4.5] Device Control Request (DR_CONTROL_REQ) +/// +/// [2.2.1.4.5]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/30662c80-ec6e-4ed1-9004-2e6e367bb59f +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceControlRequest { + pub header: DeviceIoRequest, + pub output_buffer_length: u32, + pub input_buffer_length: u32, + pub io_control_code: T, +} + +impl DeviceControlRequest +where + T::Error: ironrdp_error::Source, +{ + const HEADERLESS_SIZE: usize = 4 // OutputBufferLength + + 4 // InputBufferLength + + 4 // IoControlCode + + 20; // Additional 20 bytes for padding + + pub fn decode(header: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(ctx: "DeviceControlRequest", in: src, size: Self::HEADERLESS_SIZE); + let output_buffer_length = src.read_u32(); + let input_buffer_length = src.read_u32(); + let io_control_code = T::try_from(src.read_u32()).map_err(|e| { + error!("Failed to parse IoCtlCode"); + invalid_field_err_with_source("DeviceControlRequest", "IoCtlCode", "invalid IoCtlCode", e) + })?; + + // Padding (20 bytes): An array of 20 bytes. Reserved. This field can be set to any value and MUST be ignored. + read_padding!(src, 20); + + Ok(Self { + header, + output_buffer_length, + input_buffer_length, + io_control_code, + }) + } +} + +/// A 32-bit unsigned integer. This field is specific to the redirected device. +pub trait IoCtlCode: TryFrom {} + +/// An IoCtlCode that can be used when the IoCtlCode is not known +/// or not important. +#[derive(Debug, PartialEq, Clone)] +pub struct AnyIoCtlCode(pub u32); + +impl TryFrom for AnyIoCtlCode { + type Error = PduError; + + fn try_from(value: u32) -> Result { + Ok(Self(value)) + } +} + +impl IoCtlCode for AnyIoCtlCode {} + +/// [2.2.1.5.5] Device Control Response (DR_CONTROL_RSP) +/// +/// [2.2.1.5.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a00fbce4-95bb-4e15-8182-be2b5ef9076a +#[derive(Debug)] +pub struct DeviceControlResponse { + pub device_io_reply: DeviceIoResponse, + /// A value of `None` represents an empty buffer, + /// such as can be seen in FreeRDP [here]. + /// + /// [here]: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L677-L684 + pub output_buffer: Option>, +} + +impl DeviceControlResponse { + const NAME: &'static str = "DR_CONTROL_RSP"; + + /// A value of `None` for `output_buffer` represents an empty buffer, + /// such as can be seen in FreeRDP [here]. + /// + /// [here]: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L677-L684 + pub fn new( + req: DeviceControlRequest, + io_status: NtStatus, + output_buffer: Option>, + ) -> Self { + Self { + device_io_reply: DeviceIoResponse { + device_id: req.header.device_id, + completion_id: req.header.completion_id, + io_status, + }, + output_buffer, + } + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.device_io_reply.encode(dst)?; + if let Some(output_buffer) = &self.output_buffer { + dst.write_u32(cast_length!( + "DeviceControlResponse", + "OutputBufferLength", + output_buffer.size() + )?); + output_buffer.encode(dst)?; + } else { + dst.write_u32(0); // OutputBufferLength + } + + Ok(()) + } + + pub fn size(&self) -> usize { + self.device_io_reply.size() // DeviceIoResponse + + 4 // OutputBufferLength + + if let Some(output_buffer) = &self.output_buffer { + output_buffer.size() // OutputBuffer + } else { + 0 // OutputBuffer + } + } +} + +/// [2.2.1.5] Device I/O Response (DR_DEVICE_IOCOMPLETION) +/// +/// [2.2.1.5]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/1c412a84-0776-4984-b35c-3f0445fcae65 +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceIoResponse { + pub device_id: u32, + pub completion_id: u32, + pub io_status: NtStatus, +} + +impl DeviceIoResponse { + const FIXED_PART_SIZE: usize = size_of::() * 3; // DeviceId, CompletionId, IoStatus + + pub fn new(device_io_request: DeviceIoRequest, io_status: NtStatus) -> Self { + Self { + device_id: device_io_request.device_id, + completion_id: device_io_request.completion_id, + io_status, + } + } + + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(ctx: "DeviceIoResponse", in: src, size: Self::FIXED_PART_SIZE); + let device_id = src.read_u32(); + let completion_id = src.read_u32(); + let io_status = NtStatus::from(src.read_u32()); + + Ok(Self { + device_id, + completion_id, + io_status, + }) + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.device_id); + dst.write_u32(self.completion_id); + dst.write_u32(self.io_status.into()); + Ok(()) + } + + pub fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +/// [2.2.3.3] Server Drive I/O Request (DR_DRIVE_CORE_DEVICE_IOREQUEST) +/// +/// [2.2.3.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/89bb51af-c54d-40fb-81c1-d1bb353c4536 +#[derive(Debug, PartialEq, Clone)] +pub enum ServerDriveIoRequest { + ServerCreateDriveRequest(DeviceCreateRequest), + ServerDriveQueryInformationRequest(ServerDriveQueryInformationRequest), + DeviceCloseRequest(DeviceCloseRequest), + ServerDriveQueryDirectoryRequest(ServerDriveQueryDirectoryRequest), + ServerDriveNotifyChangeDirectoryRequest(ServerDriveNotifyChangeDirectoryRequest), + ServerDriveQueryVolumeInformationRequest(ServerDriveQueryVolumeInformationRequest), + DeviceControlRequest(DeviceControlRequest), + DeviceReadRequest(DeviceReadRequest), + DeviceWriteRequest(DeviceWriteRequest), + ServerDriveSetInformationRequest(ServerDriveSetInformationRequest), + ServerDriveLockControlRequest(ServerDriveLockControlRequest), +} + +impl ServerDriveIoRequest { + pub fn decode(dev_io_req: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + match dev_io_req.major_function { + MajorFunction::Create => Ok(DeviceCreateRequest::decode(dev_io_req, src)?.into()), + MajorFunction::Close => Ok(DeviceCloseRequest::decode(dev_io_req).into()), + MajorFunction::Read => Ok(DeviceReadRequest::decode(dev_io_req, src)?.into()), + MajorFunction::Write => Ok(DeviceWriteRequest::decode(dev_io_req, src)?.into()), + MajorFunction::DeviceControl => Ok(DeviceControlRequest::::decode(dev_io_req, src)?.into()), + MajorFunction::QueryVolumeInformation => { + Ok(ServerDriveQueryVolumeInformationRequest::decode(dev_io_req, src)?.into()) + } + MajorFunction::SetVolumeInformation => Err(unsupported_value_err!( + "ServerDriveIoRequest::decode", + "MajorFunction", + "SetVolumeInformation".to_owned() + )), // FreeRDP doesn't implement this + MajorFunction::QueryInformation => Ok(ServerDriveQueryInformationRequest::decode(dev_io_req, src)?.into()), + MajorFunction::SetInformation => Ok(ServerDriveSetInformationRequest::decode(dev_io_req, src)?.into()), + MajorFunction::DirectoryControl => match dev_io_req.minor_function { + MinorFunction::IRP_MN_QUERY_DIRECTORY => { + Ok(ServerDriveQueryDirectoryRequest::decode(dev_io_req, src)?.into()) + } + MinorFunction::IRP_MN_NOTIFY_CHANGE_DIRECTORY => { + Ok(ServerDriveNotifyChangeDirectoryRequest::decode(dev_io_req, src)?.into()) + } + // If MajorFunction is set to IRP_MJ_DIRECTORY_CONTROL and MinorFunction is set to any other value, we've encountered a server bug. + _ => Err(invalid_field_err!( + "ServerDriveIoRequest::decode", + "MinorFunction", + "invalid value" + )), + }, + MajorFunction::LockControl => Ok(ServerDriveLockControlRequest::decode(dev_io_req, src)?.into()), + } + } +} + +impl From for ServerDriveIoRequest { + fn from(req: DeviceCreateRequest) -> Self { + Self::ServerCreateDriveRequest(req) + } +} + +impl From for ServerDriveIoRequest { + fn from(req: ServerDriveQueryInformationRequest) -> Self { + Self::ServerDriveQueryInformationRequest(req) + } +} + +impl From for ServerDriveIoRequest { + fn from(req: DeviceCloseRequest) -> Self { + Self::DeviceCloseRequest(req) + } +} + +impl From for ServerDriveIoRequest { + fn from(req: ServerDriveQueryDirectoryRequest) -> Self { + Self::ServerDriveQueryDirectoryRequest(req) + } +} + +impl From for ServerDriveIoRequest { + fn from(req: ServerDriveNotifyChangeDirectoryRequest) -> Self { + Self::ServerDriveNotifyChangeDirectoryRequest(req) + } +} + +impl From for ServerDriveIoRequest { + fn from(req: ServerDriveQueryVolumeInformationRequest) -> Self { + Self::ServerDriveQueryVolumeInformationRequest(req) + } +} + +impl From> for ServerDriveIoRequest { + fn from(req: DeviceControlRequest) -> Self { + Self::DeviceControlRequest(req) + } +} + +impl From for ServerDriveIoRequest { + fn from(req: DeviceReadRequest) -> Self { + Self::DeviceReadRequest(req) + } +} + +impl From for ServerDriveIoRequest { + fn from(req: DeviceWriteRequest) -> Self { + Self::DeviceWriteRequest(req) + } +} + +impl From for ServerDriveIoRequest { + fn from(req: ServerDriveSetInformationRequest) -> Self { + Self::ServerDriveSetInformationRequest(req) + } +} + +impl From for ServerDriveIoRequest { + fn from(req: ServerDriveLockControlRequest) -> Self { + Self::ServerDriveLockControlRequest(req) + } +} + +/// [2.2.3.3.1] Server Create Drive Request (DR_DRIVE_CREATE_REQ) +/// and [2.2.1.4.1] Device Create Request (DR_CREATE_REQ) +/// +/// [2.2.3.3.1]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/95b16fd0-d530-407c-a310-adedc85e9897 +/// [2.2.1.4.1]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/5f71f6d2-d9ff-40c2-bdb5-a739447d3c3e +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceCreateRequest { + /// The MajorFunction field in this header MUST be set to IRP_MJ_CREATE. + pub device_io_request: DeviceIoRequest, + pub desired_access: DesiredAccess, + pub allocation_size: u64, + pub file_attributes: FileAttributes, + pub shared_access: SharedAccess, + pub create_disposition: CreateDisposition, + pub create_options: CreateOptions, + pub path: String, +} + +impl DeviceCreateRequest { + const FIXED_PART_SIZE: usize = 4 // DesiredAccess + + 8 // AllocationSize + + 4 // FileAttributes + + 4 // SharedAccess + + 4 // CreateDisposition + + 4 // CreateOptions + + 4; // PathLength + + fn decode(dev_io_req: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(ctx: "DeviceCreateRequest", in: src, size: Self::FIXED_PART_SIZE); + let desired_access = DesiredAccess::from_bits_retain(src.read_u32()); + let allocation_size = src.read_u64(); + let file_attributes = FileAttributes::from_bits_retain(src.read_u32()); + let shared_access = SharedAccess::from_bits_retain(src.read_u32()); + let create_disposition = CreateDisposition::from_bits_retain(src.read_u32()); + let create_options = CreateOptions::from_bits_retain(src.read_u32()); + let path_length: usize = cast_length!("DeviceCreateRequest", "path_length", src.read_u32())?; + + ensure_size!(ctx: "DeviceCreateRequest", in: src, size: path_length); + let path = from_utf16_bytes(src.read_slice(path_length)) + .trim_end_matches('\0') + .into(); + + Ok(Self { + device_io_request: dev_io_req, + desired_access, + allocation_size, + file_attributes, + shared_access, + create_disposition, + create_options, + path, + }) + } +} + +bitflags! { + /// DesiredAccess can be interpreted as either + /// [2.2.13.1.1] File_Pipe_Printer_Access_Mask \[MS-SMB2\] or [2.2.13.1.2] Directory_Access_Mask \[MS-SMB2\] + /// + /// This implements the combination of the two. For flags where the names and/or functions are distinct between the two, + /// the names are appended with an "_OR_", and the File_Pipe_Printer_Access_Mask functionality is described on the top line comment, + /// and the Directory_Access_Mask functionality is described on the bottom (2nd) line comment. + /// + /// [2.2.13.1.1]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/77b36d0f-6016-458a-a7a0-0f4a72ae1534 + /// [2.2.13.1.2]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/0a5934b1-80f1-4da0-b1bf-5e021c309b71 + #[derive(Debug, PartialEq, Clone)] + pub struct DesiredAccess: u32 { + /// This value indicates the right to read data from the file or named pipe. + /// + /// This value indicates the right to enumerate the contents of the directory. + const FILE_READ_DATA_OR_FILE_LIST_DIRECTORY = 0x00000001; + /// This value indicates the right to write data into the file or named pipe beyond the end of the file. + /// + /// This value indicates the right to create a file under the directory. + const FILE_WRITE_DATA_OR_FILE_ADD_FILE = 0x00000002; + /// This value indicates the right to append data into the file or named pipe. + /// + /// This value indicates the right to add a sub-directory under the directory. + const FILE_APPEND_DATA_OR_FILE_ADD_SUBDIRECTORY = 0x00000004; + /// This value indicates the right to read the extended attributes of the file or named pipe. + const FILE_READ_EA = 0x00000008; + /// This value indicates the right to write or change the extended attributes to the file or named pipe. + const FILE_WRITE_EA = 0x00000010; + /// This value indicates the right to traverse this directory if the server enforces traversal checking. + const FILE_TRAVERSE = 0x00000020; + /// This value indicates the right to delete entries within a directory. + const FILE_DELETE_CHILD = 0x00000040; + /// This value indicates the right to execute the file/directory. + const FILE_EXECUTE = 0x00000020; + /// This value indicates the right to read the attributes of the file/directory. + const FILE_READ_ATTRIBUTES = 0x00000080; + /// This value indicates the right to change the attributes of the file/directory. + const FILE_WRITE_ATTRIBUTES = 0x00000100; + /// This value indicates the right to delete the file/directory. + const DELETE = 0x00010000; + /// This value indicates the right to read the security descriptor for the file/directory or named pipe. + const READ_CONTROL = 0x00020000; + /// This value indicates the right to change the discretionary access control list (DACL) in the security descriptor for the file/directory or named pipe. For the DACL data pub structure, see ACL in [MS-DTYP]. + const WRITE_DAC = 0x00040000; + /// This value indicates the right to change the owner in the security descriptor for the file/directory or named pipe. + const WRITE_OWNER = 0x00080000; + /// SMB2 clients set this flag to any value. SMB2 servers SHOULD ignore this flag. + const SYNCHRONIZE = 0x00100000; + /// This value indicates the right to read or change the system access control list (SACL) in the security descriptor for the file/directory or named pipe. For the SACL data pub structure, see ACL in [MS-DTYP]. + const ACCESS_SYSTEM_SECURITY = 0x01000000; + /// This value indicates that the client is requesting an open to the file with the highest level of access the client has on this file. If no access is granted for the client on this file, the server MUST fail the open with STATUS_ACCESS_DENIED. + const MAXIMUM_ALLOWED = 0x02000000; + /// This value indicates a request for all the access flags that are previously listed except MAXIMUM_ALLOWED and ACCESS_SYSTEM_SECURITY. + const GENERIC_ALL = 0x10000000; + /// This value indicates a request for the following combination of access flags listed above: FILE_READ_ATTRIBUTES| FILE_EXECUTE| SYNCHRONIZE| READ_CONTROL. + const GENERIC_EXECUTE = 0x20000000; + /// This value indicates a request for the following combination of access flags listed above: FILE_WRITE_DATA| FILE_APPEND_DATA| FILE_WRITE_ATTRIBUTES| FILE_WRITE_EA| SYNCHRONIZE| READ_CONTROL. + const GENERIC_WRITE = 0x40000000; + /// This value indicates a request for the following combination of access flags listed above: FILE_READ_DATA| FILE_READ_ATTRIBUTES| FILE_READ_EA| SYNCHRONIZE| READ_CONTROL. + const GENERIC_READ = 0x80000000; + } +} + +bitflags! { + /// [2.6] File Attributes \[MS-FSCC\] + /// + /// [2.6]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ca28ec38-f155-4768-81d6-4bfeb8586fc9 + #[derive(Debug, PartialEq, Clone)] + pub struct FileAttributes: u32 { + const FILE_ATTRIBUTE_READONLY = 0x00000001; + const FILE_ATTRIBUTE_HIDDEN = 0x00000002; + const FILE_ATTRIBUTE_SYSTEM = 0x00000004; + const FILE_ATTRIBUTE_DIRECTORY = 0x00000010; + const FILE_ATTRIBUTE_ARCHIVE = 0x00000020; + const FILE_ATTRIBUTE_NORMAL = 0x00000080; + const FILE_ATTRIBUTE_TEMPORARY = 0x00000100; + const FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200; + const FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400; + const FILE_ATTRIBUTE_COMPRESSED = 0x00000800; + const FILE_ATTRIBUTE_OFFLINE = 0x00001000; + const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000; + const FILE_ATTRIBUTE_ENCRYPTED = 0x00004000; + const FILE_ATTRIBUTE_INTEGRITY_STREAM = 0x00008000; + const FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x00020000; + const FILE_ATTRIBUTE_RECALL_ON_OPEN = 0x00040000; + const FILE_ATTRIBUTE_PINNED = 0x00080000; + const FILE_ATTRIBUTE_UNPINNED = 0x00100000; + const FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = 0x00400000; + + const _ = !0; + } +} + +bitflags! { + /// Specified in [2.2.13] SMB2 CREATE Request + /// + /// [2.2.13]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e8fb45c1-a03d-44ca-b7ae-47385cfd7997 + #[derive(Debug, PartialEq, Clone)] + pub struct SharedAccess: u32 { + const FILE_SHARE_READ = 0x00000001; + const FILE_SHARE_WRITE = 0x00000002; + const FILE_SHARE_DELETE = 0x00000004; + } +} + +bitflags! { + /// Defined in [2.2.13] SMB2 CREATE Request + /// + /// See FreeRDP's [drive_file.c] for context about how these should be interpreted. + /// + /// [2.2.13]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e8fb45c1-a03d-44ca-b7ae-47385cfd7997 + /// [drive_file.c]: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L207 + #[derive(PartialEq, Eq, Debug, Clone)] + pub struct CreateDisposition: u32 { + const FILE_SUPERSEDE = 0x00000000; + const FILE_OPEN = 0x00000001; + const FILE_CREATE = 0x00000002; + const FILE_OPEN_IF = 0x00000003; + const FILE_OVERWRITE = 0x00000004; + const FILE_OVERWRITE_IF = 0x00000005; + } +} + +bitflags! { + /// Defined in [2.2.13] SMB2 CREATE Request + /// + /// [2.2.13]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e8fb45c1-a03d-44ca-b7ae-47385cfd7997 + #[derive(Debug, PartialEq, Clone)] + pub struct CreateOptions: u32 { + const FILE_DIRECTORY_FILE = 0x00000001; + const FILE_WRITE_THROUGH = 0x00000002; + const FILE_SEQUENTIAL_ONLY = 0x00000004; + const FILE_NO_INTERMEDIATE_BUFFERING = 0x00000008; + const FILE_SYNCHRONOUS_IO_ALERT = 0x00000010; + const FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020; + const FILE_NON_DIRECTORY_FILE = 0x00000040; + const FILE_COMPLETE_IF_OPLOCKED = 0x00000100; + const FILE_NO_EA_KNOWLEDGE = 0x00000200; + const FILE_RANDOM_ACCESS = 0x00000800; + const FILE_DELETE_ON_CLOSE = 0x00001000; + const FILE_OPEN_BY_FILE_ID = 0x00002000; + const FILE_OPEN_FOR_BACKUP_INTENT = 0x00004000; + const FILE_NO_COMPRESSION = 0x00008000; + const FILE_OPEN_REMOTE_INSTANCE = 0x00000400; + const FILE_OPEN_REQUIRING_OPLOCK = 0x00010000; + const FILE_DISALLOW_EXCLUSIVE = 0x00020000; + const FILE_RESERVE_OPFILTER = 0x00100000; + const FILE_OPEN_REPARSE_POINT = 0x00200000; + const FILE_OPEN_NO_RECALL = 0x00400000; + const FILE_OPEN_FOR_FREE_SPACE_QUERY = 0x00800000; + } +} + +/// [2.2.1.5.1] Device Create Response (DR_CREATE_RSP) +/// +/// [2.2.1.5.1]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/99e5fca5-b37a-41e4-bc69-8d7da7860f76 +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceCreateResponse { + pub device_io_reply: DeviceIoResponse, + pub file_id: u32, + pub information: Information, +} + +impl DeviceCreateResponse { + const NAME: &'static str = "DR_CREATE_RSP"; + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.device_io_reply.encode(dst)?; + dst.write_u32(self.file_id); + dst.write_u8(self.information.bits()); + Ok(()) + } + + pub fn size(&self) -> usize { + self.device_io_reply.size() // DeviceIoReply + + 4 // FileId + + 1 // Information + } +} + +bitflags! { + /// Defined in [2.2.1.5.1] Device Create Response (DR_CREATE_RSP) + /// + /// [2.2.1.5.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/99e5fca5-b37a-41e4-bc69-8d7da7860f76 + #[derive(Debug, PartialEq, Clone)] + pub struct Information: u8 { + /// A new file was created. + const FILE_SUPERSEDED = 0x00000000; + /// An existing file was opened. + const FILE_OPENED = 0x00000001; + /// An existing file was overwritten. + const FILE_OVERWRITTEN = 0x00000003; + } +} + +/// [2.2.3.3.8] Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ) +/// +/// Note that Length, Padding, and QueryBuffer fields are all ignored in keeping with the [analogous code in FreeRDP]. +/// +/// [2.2.3.3.8]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e43dcd68-2980-40a9-9238-344b6cf94946 +/// [analogous code in FreeRDP]: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L384 +#[derive(Debug, PartialEq, Clone)] +pub struct ServerDriveQueryInformationRequest { + pub device_io_request: DeviceIoRequest, + pub file_info_class_lvl: FileInformationClassLevel, +} + +impl ServerDriveQueryInformationRequest { + pub fn decode(dev_io_req: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(ctx: "ServerDriveQueryInformationRequest", in: src, size: 4); + let file_info_class_lvl = FileInformationClassLevel::from(src.read_u32()); + + Ok(Self { + device_io_request: dev_io_req, + file_info_class_lvl, + }) + } +} + +/// [2.4] File Information Classes \[MS-FSCC\] +/// +/// [2.4]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/4718fc40-e539-4014-8e33-b675af74e3e1 +#[derive(PartialEq, Eq, Clone)] +pub struct FileInformationClassLevel(u32); + +impl FileInformationClassLevel { + /// FileBasicInformation + pub const FILE_BASIC_INFORMATION: Self = Self(4); + /// FileStandardInformation + pub const FILE_STANDARD_INFORMATION: Self = Self(5); + /// FileAttributeTagInformation + pub const FILE_ATTRIBUTE_TAG_INFORMATION: Self = Self(35); + /// FileDirectoryInformation + pub const FILE_DIRECTORY_INFORMATION: Self = Self(1); + /// FileFullDirectoryInformation + pub const FILE_FULL_DIRECTORY_INFORMATION: Self = Self(2); + /// FileBothDirectoryInformation + pub const FILE_BOTH_DIRECTORY_INFORMATION: Self = Self(3); + /// FileNamesInformation + pub const FILE_NAMES_INFORMATION: Self = Self(12); + /// FileEndOfFileInformation + pub const FILE_END_OF_FILE_INFORMATION: Self = Self(20); + /// FileDispositionInformation + pub const FILE_DISPOSITION_INFORMATION: Self = Self(13); + /// FileRenameInformation + pub const FILE_RENAME_INFORMATION: Self = Self(10); + /// FileAllocationInformation + pub const FILE_ALLOCATION_INFORMATION: Self = Self(19); +} + +impl Display for FileInformationClassLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + FileInformationClassLevel::FILE_BASIC_INFORMATION => write!(f, "FileBasicInformation"), + FileInformationClassLevel::FILE_STANDARD_INFORMATION => write!(f, "FileStandardInformation"), + FileInformationClassLevel::FILE_ATTRIBUTE_TAG_INFORMATION => write!(f, "FileAttributeTagInformation"), + FileInformationClassLevel::FILE_DIRECTORY_INFORMATION => write!(f, "FileDirectoryInformation"), + FileInformationClassLevel::FILE_FULL_DIRECTORY_INFORMATION => write!(f, "FileFullDirectoryInformation"), + FileInformationClassLevel::FILE_BOTH_DIRECTORY_INFORMATION => write!(f, "FileBothDirectoryInformation"), + FileInformationClassLevel::FILE_NAMES_INFORMATION => write!(f, "FileNamesInformation"), + FileInformationClassLevel::FILE_END_OF_FILE_INFORMATION => write!(f, "FileEndOfFileInformation"), + FileInformationClassLevel::FILE_DISPOSITION_INFORMATION => write!(f, "FileDispositionInformation"), + FileInformationClassLevel::FILE_RENAME_INFORMATION => write!(f, "FileRenameInformation"), + FileInformationClassLevel::FILE_ALLOCATION_INFORMATION => write!(f, "FileAllocationInformation"), + _ => write!(f, "FileInformationClassLevel({})", self.0), + } + } +} + +impl Debug for FileInformationClassLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + FileInformationClassLevel::FILE_BASIC_INFORMATION => write!(f, "FileBasicInformation"), + FileInformationClassLevel::FILE_STANDARD_INFORMATION => write!(f, "FileStandardInformation"), + FileInformationClassLevel::FILE_ATTRIBUTE_TAG_INFORMATION => write!(f, "FileAttributeTagInformation"), + FileInformationClassLevel::FILE_DIRECTORY_INFORMATION => write!(f, "FileDirectoryInformation"), + FileInformationClassLevel::FILE_FULL_DIRECTORY_INFORMATION => write!(f, "FileFullDirectoryInformation"), + FileInformationClassLevel::FILE_BOTH_DIRECTORY_INFORMATION => write!(f, "FileBothDirectoryInformation"), + FileInformationClassLevel::FILE_NAMES_INFORMATION => write!(f, "FileNamesInformation"), + _ => write!(f, "FileInformationClassLevel({})", self.0), + } + } +} + +impl From for FileInformationClassLevel { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(file_info_class_lvl: FileInformationClassLevel) -> Self { + file_info_class_lvl.0 + } +} + +/// [2.2.3.4.8] Client Drive Query Information Response (DR_DRIVE_QUERY_INFORMATION_RSP) +/// +/// [2.2.3.4.8]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/37ef4fb1-6a95-4200-9fbf-515464f034a4 +#[derive(Debug, PartialEq, Clone)] +pub struct ClientDriveQueryInformationResponse { + pub device_io_response: DeviceIoResponse, + /// If [`Self::device_io_response`] has an `io_status` besides [`NtStatus::SUCCESS`], + /// this field can be omitted (set to `None`). + pub buffer: Option, +} + +impl ClientDriveQueryInformationResponse { + const NAME: &'static str = "DR_DRIVE_QUERY_INFORMATION_RSP"; + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.device_io_response.encode(dst)?; + if let Some(buffer) = &self.buffer { + dst.write_u32(cast_length!( + "ClientDriveQueryInformationResponse", + "buffer.size()", + buffer.size() + )?); + buffer.encode(dst)?; + } else { + dst.write_u32(0); // Length = 0 + } + Ok(()) + } + + pub fn size(&self) -> usize { + self.device_io_response.size() // DeviceIoResponse + + 4 // Length + + if let Some(buffer) = &self.buffer { + buffer.size() // Buffer + } else { + 0 + } + } +} + +/// [2.4] File Information Classes \[MS-FSCC\] +/// +/// [2.4]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/4718fc40-e539-4014-8e33-b675af74e3e1 +#[derive(Debug, PartialEq, Clone)] +pub enum FileInformationClass { + Basic(FileBasicInformation), + Standard(FileStandardInformation), + AttributeTag(FileAttributeTagInformation), + BothDirectory(FileBothDirectoryInformation), + FullDirectory(FileFullDirectoryInformation), + Names(FileNamesInformation), + Directory(FileDirectoryInformation), + EndOfFile(FileEndOfFileInformation), + Disposition(FileDispositionInformation), + Rename(FileRenameInformation), + Allocation(FileAllocationInformation), +} + +impl FileInformationClass { + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + match self { + Self::Basic(f) => f.encode(dst), + Self::Standard(f) => f.encode(dst), + Self::AttributeTag(f) => f.encode(dst), + Self::BothDirectory(f) => f.encode(dst), + Self::FullDirectory(f) => f.encode(dst), + Self::Names(f) => f.encode(dst), + Self::Directory(f) => f.encode(dst), + _ => Err(unsupported_value_err!( + "FileInformationClass::encode", + "FileInformationClass", + self.to_string() + )), + } + } + + pub fn decode( + file_info_class_level: FileInformationClassLevel, + length: usize, + src: &mut ReadCursor<'_>, + ) -> DecodeResult { + match file_info_class_level { + FileInformationClassLevel::FILE_BASIC_INFORMATION => Ok(FileBasicInformation::decode(src)?.into()), + FileInformationClassLevel::FILE_END_OF_FILE_INFORMATION => { + Ok(FileEndOfFileInformation::decode(src)?.into()) + } + FileInformationClassLevel::FILE_DISPOSITION_INFORMATION => { + Ok(FileDispositionInformation::decode(src, length)?.into()) + } + FileInformationClassLevel::FILE_RENAME_INFORMATION => Ok(FileRenameInformation::decode(src)?.into()), + FileInformationClassLevel::FILE_ALLOCATION_INFORMATION => { + Ok(FileAllocationInformation::decode(src)?.into()) + } + _ => Err(unsupported_value_err!( + "FileInformationClass::decode", + "FileInformationClassLevel", + file_info_class_level.to_string() + )), + } + } + + pub fn size(&self) -> usize { + match self { + Self::Basic(_) => FileBasicInformation::size(), + Self::Standard(_) => FileStandardInformation::size(), + Self::AttributeTag(_) => FileAttributeTagInformation::size(), + Self::BothDirectory(f) => f.size(), + Self::FullDirectory(f) => f.size(), + Self::Names(f) => f.size(), + Self::Directory(f) => f.size(), + Self::EndOfFile(_) => FileEndOfFileInformation::size(), + Self::Disposition(_) => FileDispositionInformation::size(), + Self::Rename(f) => f.size(), + Self::Allocation(_) => FileAllocationInformation::size(), + } + } +} + +impl Display for FileInformationClass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Basic(_) => write!(f, "FileBasicInformation"), + Self::Standard(_) => write!(f, "FileStandardInformation"), + Self::AttributeTag(_) => write!(f, "FileAttributeTagInformation"), + Self::BothDirectory(_) => write!(f, "FileBothDirectoryInformation"), + Self::FullDirectory(_) => write!(f, "FileFullDirectoryInformation"), + Self::Names(_) => write!(f, "FileNamesInformation"), + Self::Directory(_) => write!(f, "FileDirectoryInformation"), + Self::EndOfFile(_) => write!(f, "FileEndOfFileInformation"), + Self::Disposition(_) => write!(f, "FileDispositionInformation"), + Self::Rename(_) => write!(f, "FileRenameInformation"), + Self::Allocation(_) => write!(f, "FileAllocationInformation"), + } + } +} + +impl From for FileInformationClass { + fn from(f: FileBasicInformation) -> Self { + Self::Basic(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileStandardInformation) -> Self { + Self::Standard(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileAttributeTagInformation) -> Self { + Self::AttributeTag(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileBothDirectoryInformation) -> Self { + Self::BothDirectory(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileFullDirectoryInformation) -> Self { + Self::FullDirectory(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileNamesInformation) -> Self { + Self::Names(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileDirectoryInformation) -> Self { + Self::Directory(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileEndOfFileInformation) -> Self { + Self::EndOfFile(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileDispositionInformation) -> Self { + Self::Disposition(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileRenameInformation) -> Self { + Self::Rename(f) + } +} + +impl From for FileInformationClass { + fn from(f: FileAllocationInformation) -> Self { + Self::Allocation(f) + } +} + +/// [2.4.7] FileBasicInformation \[MS-FSCC\] +/// +/// [2.4.7]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/16023025-8a78-492f-8b96-c873b042ac50 +#[derive(Debug, PartialEq, Clone)] +pub struct FileBasicInformation { + pub creation_time: i64, + pub last_access_time: i64, + pub last_write_time: i64, + pub change_time: i64, + pub file_attributes: FileAttributes, + // NOTE: The `reserved` field in the spec MUST not be serialized and sent over RDP, or it will break the server implementation. + // FreeRDP does the same: https://github.com/FreeRDP/FreeRDP/blob/1adb263813ca2e76a893ef729a04db8f94b5d757/channels/drive/client/drive_file.c#L508 +} + +impl FileBasicInformation { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(ctx: "FileBasicInformation", in: src, size: Self::size()); + let creation_time = src.read_i64(); + let last_access_time = src.read_i64(); + let last_write_time = src.read_i64(); + let change_time = src.read_i64(); + let file_attributes = FileAttributes::from_bits_retain(src.read_u32()); + Ok(Self { + creation_time, + last_access_time, + last_write_time, + change_time, + file_attributes, + }) + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + dst.write_i64(self.creation_time); + dst.write_i64(self.last_access_time); + dst.write_i64(self.last_write_time); + dst.write_i64(self.change_time); + dst.write_u32(self.file_attributes.bits()); + Ok(()) + } + + pub fn size() -> usize { + 8 // CreationTime + + 8 // LastAccessTime + + 8 // LastWriteTime + + 8 // ChangeTime + + 4 // FileAttributes + } +} + +/// [2.4.41] FileStandardInformation \[MS-FSCC\] +/// +/// [2.4.41]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/5afa7f66-619c-48f3-955f-68c4ece704ae +#[derive(Debug, PartialEq, Clone)] +pub struct FileStandardInformation { + pub allocation_size: i64, + pub end_of_file: i64, + pub number_of_links: u32, + /// Set to TRUE to indicate that a file deletion has been requested; set to FALSE + /// otherwise. + pub delete_pending: Boolean, + /// Set to TRUE to indicate that the file is a directory; set to FALSE otherwise. + pub directory: Boolean, + // NOTE: `reserved` field omitted. +} + +impl FileStandardInformation { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + dst.write_i64(self.allocation_size); + dst.write_i64(self.end_of_file); + dst.write_u32(self.number_of_links); + dst.write_u8(self.delete_pending.into()); + dst.write_u8(self.directory.into()); + Ok(()) + } + + pub fn size() -> usize { + 8 // AllocationSize + + 8 // EndOfFile + + 4 // NumberOfLinks + + 1 // DeletePending + + 1 // Directory + } +} + +/// [2.1.8] Boolean +/// +/// [2.1.8]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/8ce7b38c-d3cc-415d-ab39-944000ea77ff +#[derive(Debug, PartialEq, Clone, Copy)] +#[repr(u8)] +pub enum Boolean { + True = 1, + False = 0, +} + +impl From for u8 { + fn from(boolean: Boolean) -> Self { + match boolean { + Boolean::True => 1, + Boolean::False => 0, + } + } +} + +impl From for Boolean { + fn from(value: u8) -> Self { + match value { + 1 => Boolean::True, + _ => Boolean::False, + } + } +} + +/// [2.4.6] FileAttributeTagInformation \[MS-FSCC\] +/// +/// [2.4.6]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e?redirectedfrom=MSDN +#[derive(Debug, PartialEq, Clone)] +pub struct FileAttributeTagInformation { + pub file_attributes: FileAttributes, + pub reparse_tag: u32, +} + +impl FileAttributeTagInformation { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + dst.write_u32(self.file_attributes.bits()); + dst.write_u32(self.reparse_tag); + Ok(()) + } + + fn size() -> usize { + 4 // FileAttributes + + 4 // ReparseTag + } +} + +/// [2.4.8] FileBothDirectoryInformation \[MS-FSCC\] +/// +/// [2.4.8]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/270df317-9ba5-4ccb-ba00-8d22be139bc5 +#[derive(Debug, PartialEq, Clone)] +pub struct FileBothDirectoryInformation { + pub next_entry_offset: u32, + pub file_index: u32, + pub creation_time: i64, + pub last_access_time: i64, + pub last_write_time: i64, + pub change_time: i64, + pub end_of_file: i64, + pub allocation_size: i64, + pub file_attributes: FileAttributes, + pub ea_size: u32, + pub short_name_length: i8, + // reserved: u8: MUST NOT be added, + // see https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L907 + pub short_name: [u8; 24], // 24 bytes + pub file_name: String, +} + +impl FileBothDirectoryInformation { + pub fn new( + creation_time: i64, + last_access_time: i64, + last_write_time: i64, + change_time: i64, + file_size: i64, + file_attributes: FileAttributes, + file_name: String, + ) -> Self { + // Default field values taken from + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L871 + Self { + next_entry_offset: 0, + file_index: 0, + creation_time, + last_access_time, + last_write_time, + change_time, + end_of_file: file_size, + allocation_size: file_size, + file_attributes, + ea_size: 0, + short_name_length: 0, + short_name: [0; 24], + file_name, + } + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.next_entry_offset); + dst.write_u32(self.file_index); + dst.write_i64(self.creation_time); + dst.write_i64(self.last_access_time); + dst.write_i64(self.last_write_time); + dst.write_i64(self.change_time); + dst.write_i64(self.end_of_file); + dst.write_i64(self.allocation_size); + dst.write_u32(self.file_attributes.bits()); + dst.write_u32(cast_length!( + "FileBothDirectoryInformation::encode", + "file_name_length", + encoded_str_len(&self.file_name, CharacterSet::Unicode, false) + )?); + dst.write_u32(self.ea_size); + dst.write_i8(self.short_name_length); + // reserved u8 MUST NOT be added, + // see https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L907 + dst.write_slice(&self.short_name); + write_string_to_cursor(dst, &self.file_name, CharacterSet::Unicode, false)?; + Ok(()) + } + + fn size(&self) -> usize { + 4 // NextEntryOffset + + 4 // FileIndex + + 8 // CreationTime + + 8 // LastAccessTime + + 8 // LastWriteTime + + 8 // ChangeTime + + 8 // EndOfFile + + 8 // AllocationSize + + 4 // FileAttributes + + 4 // FileNameLength + + 4 // EaSize + + 1 // ShortNameLength + + 24 // ShortName + + encoded_str_len(&self.file_name, CharacterSet::Unicode, false) + } +} + +/// [2.4.14] FileFullDirectoryInformation \[MS-FSCC\] +/// +/// [2.4.14]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/e8d926d1-3a22-4654-be9c-58317a85540b +#[derive(Debug, PartialEq, Clone)] +pub struct FileFullDirectoryInformation { + pub next_entry_offset: u32, + pub file_index: u32, + pub creation_time: i64, + pub last_access_time: i64, + pub last_write_time: i64, + pub change_time: i64, + pub end_of_file: i64, + pub allocation_size: i64, + pub file_attributes: FileAttributes, + pub ea_size: u32, + pub file_name: String, +} + +impl FileFullDirectoryInformation { + pub fn new( + creation_time: i64, + last_access_time: i64, + last_write_time: i64, + change_time: i64, + file_size: i64, + file_attributes: FileAttributes, + file_name: String, + ) -> Self { + // Default field values taken from + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L871 + Self { + next_entry_offset: 0, + file_index: 0, + creation_time, + last_access_time, + last_write_time, + change_time, + end_of_file: file_size, + allocation_size: file_size, + file_attributes, + ea_size: 0, + file_name, + } + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.next_entry_offset); + dst.write_u32(self.file_index); + dst.write_i64(self.creation_time); + dst.write_i64(self.last_access_time); + dst.write_i64(self.last_write_time); + dst.write_i64(self.change_time); + dst.write_i64(self.end_of_file); + dst.write_i64(self.allocation_size); + dst.write_u32(self.file_attributes.bits()); + dst.write_u32(cast_length!( + "FileFullDirectoryInformation::encode", + "file_name_length", + encoded_str_len(&self.file_name, CharacterSet::Unicode, false) + )?); + dst.write_u32(self.ea_size); + write_string_to_cursor(dst, &self.file_name, CharacterSet::Unicode, false)?; + Ok(()) + } + + fn size(&self) -> usize { + 4 // NextEntryOffset + + 4 // FileIndex + + 8 // CreationTime + + 8 // LastAccessTime + + 8 // LastWriteTime + + 8 // ChangeTime + + 8 // EndOfFile + + 8 // AllocationSize + + 4 // FileAttributes + + 4 // FileNameLength + + 4 // EaSize + + encoded_str_len(&self.file_name, CharacterSet::Unicode, false) + } +} + +/// [2.4.28] FileNamesInformation \[MS-FSCC\] +/// +/// [2.4.28]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/a289f7a8-83d2-4927-8c88-b2d328dde5a5?redirectedfrom=MSDN +#[derive(Debug, PartialEq, Clone)] +pub struct FileNamesInformation { + pub next_entry_offset: u32, + pub file_index: u32, + pub file_name: String, +} + +impl FileNamesInformation { + pub fn new(file_name: String) -> Self { + // Default field values taken from + // https://github.com/FreeRDP/FreeRDP/blob/dfa231c0a55b005af775b833f92f6bcd30363d77/channels/drive/client/drive_file.c#L912 + Self { + next_entry_offset: 0, + file_index: 0, + file_name, + } + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.next_entry_offset); + dst.write_u32(self.file_index); + dst.write_u32(cast_length!( + "FileNamesInformation::encode", + "file_name_length", + encoded_str_len(&self.file_name, CharacterSet::Unicode, false) + )?); + write_string_to_cursor(dst, &self.file_name, CharacterSet::Unicode, false)?; + Ok(()) + } + + fn size(&self) -> usize { + 4 // NextEntryOffset + + 4 // FileIndex + + 4 // FileNameLength + + encoded_str_len(&self.file_name, CharacterSet::Unicode, false) + } +} + +/// [2.4.10] FileDirectoryInformation \[MS-FSCC\] +/// +/// [2.4.10]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/b38bf518-9057-4c88-9ddd-5e2d3976a64b +#[derive(Debug, PartialEq, Clone)] +pub struct FileDirectoryInformation { + pub next_entry_offset: u32, + pub file_index: u32, + pub creation_time: i64, + pub last_access_time: i64, + pub last_write_time: i64, + pub change_time: i64, + pub end_of_file: i64, + pub allocation_size: i64, + pub file_attributes: FileAttributes, + pub file_name: String, +} + +impl FileDirectoryInformation { + pub fn new( + creation_time: i64, + last_access_time: i64, + last_write_time: i64, + change_time: i64, + file_size: i64, + file_attributes: FileAttributes, + file_name: String, + ) -> Self { + // Default field values taken from + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L796 + Self { + next_entry_offset: 0, + file_index: 0, + creation_time, + last_access_time, + last_write_time, + change_time, + end_of_file: file_size, + allocation_size: file_size, + file_attributes, + file_name, + } + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.next_entry_offset); + dst.write_u32(self.file_index); + dst.write_i64(self.creation_time); + dst.write_i64(self.last_access_time); + dst.write_i64(self.last_write_time); + dst.write_i64(self.change_time); + dst.write_i64(self.end_of_file); + dst.write_i64(self.allocation_size); + dst.write_u32(self.file_attributes.bits()); + dst.write_u32(cast_length!( + "FileDirectoryInformation::encode", + "file_name_length", + encoded_str_len(&self.file_name, CharacterSet::Unicode, false) + )?); + write_string_to_cursor(dst, &self.file_name, CharacterSet::Unicode, false)?; + Ok(()) + } + + fn size(&self) -> usize { + 4 // NextEntryOffset + + 4 // FileIndex + + 8 // CreationTime + + 8 // LastAccessTime + + 8 // LastWriteTime + + 8 // ChangeTime + + 8 // EndOfFile + + 8 // AllocationSize + + 4 // FileAttributes + + 4 // FileNameLength + + encoded_str_len(&self.file_name, CharacterSet::Unicode, false) + } +} + +/// [2.2.1.4.2] Device Close Request (DR_CLOSE_REQ) +/// +/// [2.2.1.4.2]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/3ec6627f-9e0f-4941-a828-3fc6ed63d9e7 +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceCloseRequest { + pub device_io_request: DeviceIoRequest, + // Padding (32 bytes): ignored as per FreeRDP: + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L236 +} + +impl DeviceCloseRequest { + pub fn decode(dev_io_req: DeviceIoRequest) -> Self { + Self { + device_io_request: dev_io_req, + } + } +} + +/// [2.2.1.5.2] Device Close Response (DR_CLOSE_RSP) +/// +/// [2.2.1.5.2]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/0dae7031-cfd8-4f14-908c-ec06e14997b5 +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceCloseResponse { + pub device_io_response: DeviceIoResponse, + // Padding (4 bytes): An array of 4 bytes. Reserved. This field can be set to any value and MUST be ignored. +} + +impl DeviceCloseResponse { + const NAME: &'static str = "DR_CLOSE_RSP"; + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.device_io_response.encode(dst)?; + dst.write_u32(0); // Padding + Ok(()) + } + + pub fn size(&self) -> usize { + self.device_io_response.size() // DeviceIoResponse + + 4 // Padding + } +} + +/// [2.2.3.3.10] Server Drive Query Directory Request (DR_DRIVE_QUERY_DIRECTORY_REQ) +/// +/// [2.2.3.3.10]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/458019d2-5d5a-4fd4-92ef-8c05f8d7acb1 +#[derive(Debug, PartialEq, Clone)] +pub struct ServerDriveQueryDirectoryRequest { + pub device_io_request: DeviceIoRequest, + pub file_info_class_lvl: FileInformationClassLevel, + pub initial_query: u8, + pub path: String, +} + +impl ServerDriveQueryDirectoryRequest { + const FIXED_PART_SIZE: usize = 4 /* FsInformationClass */ + 1 /* InitialQuery */ + 4 /* PathLength */ + 23 /* Padding */; + + fn decode(device_io_request: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let file_info_class_lvl = FileInformationClassLevel::from(src.read_u32()); + + // This field MUST contain one of the following values + match file_info_class_lvl { + FileInformationClassLevel::FILE_DIRECTORY_INFORMATION + | FileInformationClassLevel::FILE_FULL_DIRECTORY_INFORMATION + | FileInformationClassLevel::FILE_BOTH_DIRECTORY_INFORMATION + | FileInformationClassLevel::FILE_NAMES_INFORMATION => {} + _ => { + return Err(invalid_field_err!( + "ServerDriveQueryDirectoryRequest::decode", + "file_info_class_lvl", + "received invalid level" + )) + } + } + + let initial_query = src.read_u8(); + let path_length = cast_length!("ServerDriveQueryDirectoryRequest", "path_length", src.read_u32())?; + // Padding (23 bytes): An array of 23 bytes. This field is unused and MUST be ignored. + read_padding!(src, 23); + + ensure_size!(in: src, size: path_length); + let path = decode_string(src.read_slice(path_length), CharacterSet::Unicode, true)?; + + Ok(Self { + device_io_request, + file_info_class_lvl, + initial_query, + path, + }) + } +} + +/// 2.2.3.3.11 Server Drive NotifyChange Directory Request (DR_DRIVE_NOTIFY_CHANGE_DIRECTORY_REQ) +/// +/// [2.2.3.3.11]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/ed05e73d-e53e-4261-a1e1-365a70ba6512 +#[derive(Debug, PartialEq, Clone)] +pub struct ServerDriveNotifyChangeDirectoryRequest { + pub device_io_request: DeviceIoRequest, + pub watch_tree: u8, + pub completion_filter: u32, +} + +impl ServerDriveNotifyChangeDirectoryRequest { + const FIXED_PART_SIZE: usize = 1 /* WatchTree */ + 4 /* CompletionFilter */ + 27 /* Padding */; + + fn decode(device_io_request: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let watch_tree = src.read_u8(); + let completion_filter = src.read_u32(); + // Padding (27 bytes): An array of 27 bytes. This field is unused and MUST be ignored. + read_padding!(src, 27); + + Ok(Self { + device_io_request, + watch_tree, + completion_filter, + }) + } +} + +/// [2.2.3.4.10] Client Drive Query Directory Response (DR_DRIVE_QUERY_DIRECTORY_RSP) +/// +/// [2.2.3.4.10]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/9c929407-a833-4893-8f20-90c984756140 +#[derive(Debug, PartialEq, Clone)] +pub struct ClientDriveQueryDirectoryResponse { + pub device_io_reply: DeviceIoResponse, + pub buffer: Option, +} + +impl ClientDriveQueryDirectoryResponse { + const NAME: &'static str = "DR_DRIVE_QUERY_DIRECTORY_RSP"; + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.device_io_reply.encode(dst)?; + dst.write_u32(cast_length!( + "ClientDriveQueryDirectoryResponse", + "length", + self.buffer.as_ref().map_or(0, |buf| buf.size()) + )?); + if let Some(buffer) = &self.buffer { + buffer.encode(dst)?; + } else { + write_padding!(dst, 1) // Padding: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L937 + } + Ok(()) + } + + pub fn size(&self) -> usize { + self.device_io_reply.size() // DeviceIoResponse + + 4 // Length + + if let Some(buffer) = &self.buffer { + buffer.size() // Buffer + } else { + 1 // Padding: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L937 + } + } +} + +/// [2.2.3.3.6] Server Drive Query Volume Information Request +/// +/// We only need to read the buffer up to the FileInformationClass to get the job done, so the rest of the fields in +/// this structure are discarded. See FreeRDP: +/// +/// +/// [2.2.3.3.6]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/484e622d-0e2b-423c-8461-7de38878effb +#[derive(Debug, PartialEq, Clone)] +pub struct ServerDriveQueryVolumeInformationRequest { + pub device_io_request: DeviceIoRequest, + pub fs_info_class_lvl: FileSystemInformationClassLevel, +} + +impl ServerDriveQueryVolumeInformationRequest { + const FIXED_PART_SIZE: usize = 4 /* FsInformationClass */ + 4 /* Length */ + 24 /* Padding */; + + pub fn decode(dev_io_req: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let fs_info_class_lvl = FileSystemInformationClassLevel::from(src.read_u32()); + + // This field MUST contain one of the following values. + match fs_info_class_lvl { + FileSystemInformationClassLevel::FILE_FS_VOLUME_INFORMATION + | FileSystemInformationClassLevel::FILE_FS_SIZE_INFORMATION + | FileSystemInformationClassLevel::FILE_FS_ATTRIBUTE_INFORMATION + | FileSystemInformationClassLevel::FILE_FS_FULL_SIZE_INFORMATION + | FileSystemInformationClassLevel::FILE_FS_DEVICE_INFORMATION => {} + _ => { + return Err(invalid_field_err!( + "ServerDriveQueryVolumeInformationRequest::decode", + "fs_info_class_lvl", + "received invalid level" + )) + } + } + + // We only need to read the buffer up to the FileInformationClass to get the job done, so the rest of the fields in + // this structure are discarded. See FreeRDP: + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L464 + let length = cast_length!("ServerDriveQueryVolumeInformationRequest", "length", src.read_u32())?; // Length + read_padding!(src, 24); // Padding + ensure_size!(in: src, size: length); + read_padding!(src, length); // QueryVolumeBuffer + + Ok(Self { + device_io_request: dev_io_req, + fs_info_class_lvl, + }) + } +} + +/// [2.5] File System Information Classes [MS-FSCC] +/// +/// [2.5] +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct FileSystemInformationClassLevel(u32); + +impl FileSystemInformationClassLevel { + /// FileFsVolumeInformation + pub const FILE_FS_VOLUME_INFORMATION: Self = Self(1); + /// FileFsLabelInformation + pub const FILE_FS_LABEL_INFORMATION: Self = Self(2); + /// FileFsSizeInformation + pub const FILE_FS_SIZE_INFORMATION: Self = Self(3); + /// FileFsDeviceInformation + pub const FILE_FS_DEVICE_INFORMATION: Self = Self(4); + /// FileFsAttributeInformation + pub const FILE_FS_ATTRIBUTE_INFORMATION: Self = Self(5); + /// FileFsControlInformation + pub const FILE_FS_CONTROL_INFORMATION: Self = Self(6); + /// FileFsFullSizeInformation + pub const FILE_FS_FULL_SIZE_INFORMATION: Self = Self(7); + /// FileFsObjectIdInformation + pub const FILE_FS_OBJECT_ID_INFORMATION: Self = Self(8); + /// FileFsDriverPathInformation + pub const FILE_FS_DRIVER_PATH_INFORMATION: Self = Self(9); + /// FileFsVolumeFlagsInformation + pub const FILE_FS_VOLUME_FLAGS_INFORMATION: Self = Self(10); + /// FileFsSectorSizeInformation + pub const FILE_FS_SECTOR_SIZE_INFORMATION: Self = Self(11); +} + +impl From for FileSystemInformationClassLevel { + fn from(value: u32) -> Self { + Self(value) + } +} + +/// [2.5] File System Information Classes +/// +/// [2.5]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ee12042a-9352-46e3-9f67-c094b75fe6c3 +#[derive(Debug, PartialEq, Clone)] +pub enum FileSystemInformationClass { + FileFsVolumeInformation(FileFsVolumeInformation), + FileFsSizeInformation(FileFsSizeInformation), + FileFsAttributeInformation(FileFsAttributeInformation), + FileFsFullSizeInformation(FileFsFullSizeInformation), + FileFsDeviceInformation(FileFsDeviceInformation), +} + +impl FileSystemInformationClass { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + match self { + Self::FileFsVolumeInformation(f) => f.encode(dst), + Self::FileFsSizeInformation(f) => f.encode(dst), + Self::FileFsAttributeInformation(f) => f.encode(dst), + Self::FileFsFullSizeInformation(f) => f.encode(dst), + Self::FileFsDeviceInformation(f) => f.encode(dst), + } + } + + fn size(&self) -> usize { + match self { + Self::FileFsVolumeInformation(f) => f.size(), + Self::FileFsSizeInformation(f) => f.size(), + Self::FileFsAttributeInformation(f) => f.size(), + Self::FileFsFullSizeInformation(_) => FileFsFullSizeInformation::size(), + Self::FileFsDeviceInformation(_) => FileFsDeviceInformation::size(), + } + } +} + +impl From for FileSystemInformationClass { + fn from(file_fs_vol_info: FileFsVolumeInformation) -> Self { + Self::FileFsVolumeInformation(file_fs_vol_info) + } +} + +impl From for FileSystemInformationClass { + fn from(file_fs_vol_info: FileFsSizeInformation) -> Self { + Self::FileFsSizeInformation(file_fs_vol_info) + } +} + +impl From for FileSystemInformationClass { + fn from(file_fs_vol_info: FileFsAttributeInformation) -> Self { + Self::FileFsAttributeInformation(file_fs_vol_info) + } +} + +impl From for FileSystemInformationClass { + fn from(file_fs_vol_info: FileFsFullSizeInformation) -> Self { + Self::FileFsFullSizeInformation(file_fs_vol_info) + } +} + +impl From for FileSystemInformationClass { + fn from(file_fs_vol_info: FileFsDeviceInformation) -> Self { + Self::FileFsDeviceInformation(file_fs_vol_info) + } +} + +/// [2.5.9] FileFsVolumeInformation +/// +/// [2.5.9]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/bf691378-c34e-4a13-976e-404ea1a87738 +#[derive(Debug, PartialEq, Clone)] +pub struct FileFsVolumeInformation { + pub volume_creation_time: i64, + pub volume_serial_number: u32, + pub supports_objects: Boolean, + // reserved is omitted + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L495 + pub volume_label: String, +} + +impl FileFsVolumeInformation { + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_i64(self.volume_creation_time); + dst.write_u32(self.volume_serial_number); + dst.write_u32(cast_length!( + "FileFsVolumeInformation::encode", + "volume_label_length", + encoded_str_len(&self.volume_label, CharacterSet::Unicode, true) + )?); + dst.write_u8(self.supports_objects.into()); + write_string_to_cursor(dst, &self.volume_label, CharacterSet::Unicode, true)?; + Ok(()) + } + + pub fn size(&self) -> usize { + 8 // VolumeCreationTime + + 4 // VolumeSerialNumber + + 4 // VolumeLabelLength + + 1 // SupportsObjects + + encoded_str_len(&self.volume_label, CharacterSet::Unicode, true) + } +} + +/// [2.5.8] FileFsSizeInformation +/// +/// [2.5.8]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/e13e068c-e3a7-4dd4-94fd-3892b492e6e7 +#[derive(Debug, PartialEq, Clone)] +pub struct FileFsSizeInformation { + pub total_alloc_units: i64, + pub available_alloc_units: i64, + pub sectors_per_alloc_unit: u32, + pub bytes_per_sector: u32, +} + +impl FileFsSizeInformation { + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_i64(self.total_alloc_units); + dst.write_i64(self.available_alloc_units); + dst.write_u32(self.sectors_per_alloc_unit); + dst.write_u32(self.bytes_per_sector); + Ok(()) + } + + pub fn size(&self) -> usize { + 8 // TotalAllocationUnits + + 8 // AvailableAllocationUnits + + 4 // SectorsPerAllocationUnit + + 4 // BytesPerSector + } +} + +/// [2.5.1] FileFsAttributeInformation +/// +/// [2.5.1]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ebc7e6e5-4650-4e54-b17c-cf60f6fbeeaa +#[derive(Debug, PartialEq, Clone)] +pub struct FileFsAttributeInformation { + pub file_system_attributes: FileSystemAttributes, + pub max_component_name_len: u32, + pub file_system_name: String, +} + +impl FileFsAttributeInformation { + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.file_system_attributes.bits()); + dst.write_u32(self.max_component_name_len); + dst.write_u32(cast_length!( + "FileFsAttributeInformation::encode", + "file_system_name_length", + encoded_str_len(&self.file_system_name, CharacterSet::Unicode, true) + )?); + write_string_to_cursor(dst, &self.file_system_name, CharacterSet::Unicode, true)?; + Ok(()) + } + + pub fn size(&self) -> usize { + 4 // FileSystemAttributes + + 4 // MaximumComponentNameLength + + 4 // FileSystemNameLength + + encoded_str_len(&self.file_system_name, CharacterSet::Unicode, true) + } +} + +/// [2.5.4] FileFsFullSizeInformation +/// +/// [2.5.4]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/63768db7-9012-4209-8cca-00781e7322f5 +#[derive(Debug, PartialEq, Clone)] +pub struct FileFsFullSizeInformation { + pub total_alloc_units: i64, + pub caller_available_alloc_units: i64, + pub actual_available_alloc_units: i64, + pub sectors_per_alloc_unit: u32, + pub bytes_per_sector: u32, +} + +impl FileFsFullSizeInformation { + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + dst.write_i64(self.total_alloc_units); + dst.write_i64(self.caller_available_alloc_units); + dst.write_i64(self.actual_available_alloc_units); + dst.write_u32(self.sectors_per_alloc_unit); + dst.write_u32(self.bytes_per_sector); + Ok(()) + } + + pub fn size() -> usize { + 8 // TotalAllocationUnits + + 8 // CallerAvailableAllocationUnits + + 8 // ActualAvailableAllocationUnits + + 4 // SectorsPerAllocationUnit + + 4 // BytesPerSector + } +} + +/// [2.5.10] FileFsDeviceInformation +/// +/// [2.5.10]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/616b66d5-b335-4e1c-8f87-b4a55e8d3e4a +#[derive(Debug, PartialEq, Clone)] +pub struct FileFsDeviceInformation { + pub device_type: u32, + pub characteristics: Characteristics, +} + +impl FileFsDeviceInformation { + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + dst.write_u32(self.device_type); + dst.write_u32(self.characteristics.bits()); + Ok(()) + } + + pub fn size() -> usize { + 4 // DeviceType + + 4 // Characteristics + } +} + +bitflags! { + /// See [2.5.1] FileFsAttributeInformation. + /// + /// [2.5.1]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ebc7e6e5-4650-4e54-b17c-cf60f6fbeeaa + #[derive(Debug, PartialEq, Clone)] + pub struct FileSystemAttributes: u32 { + const FILE_SUPPORTS_USN_JOURNAL = 0x02000000; + const FILE_SUPPORTS_OPEN_BY_FILE_ID = 0x01000000; + const FILE_SUPPORTS_EXTENDED_ATTRIBUTES = 0x00800000; + const FILE_SUPPORTS_HARD_LINKS = 0x00400000; + const FILE_SUPPORTS_TRANSACTIONS = 0x00200000; + const FILE_SEQUENTIAL_WRITE_ONCE = 0x00100000; + const FILE_READ_ONLY_VOLUME = 0x00080000; + const FILE_NAMED_STREAMS = 0x00040000; + const FILE_SUPPORTS_ENCRYPTION = 0x00020000; + const FILE_SUPPORTS_OBJECT_IDS = 0x00010000; + const FILE_VOLUME_IS_COMPRESSED = 0x00008000; + const FILE_SUPPORTS_REMOTE_STORAGE = 0x00000100; + const FILE_SUPPORTS_REPARSE_POINTS = 0x00000080; + const FILE_SUPPORTS_SPARSE_FILES = 0x00000040; + const FILE_VOLUME_QUOTAS = 0x00000020; + const FILE_FILE_COMPRESSION = 0x00000010; + const FILE_PERSISTENT_ACLS = 0x00000008; + const FILE_UNICODE_ON_DISK = 0x00000004; + const FILE_CASE_PRESERVED_NAMES = 0x00000002; + const FILE_CASE_SENSITIVE_SEARCH = 0x00000001; + const FILE_SUPPORT_INTEGRITY_STREAMS = 0x04000000; + const FILE_SUPPORTS_BLOCK_REFCOUNTING = 0x08000000; + const FILE_SUPPORTS_SPARSE_VDL = 0x10000000; + } +} + +bitflags! { + /// See [2.5.10] FileFsDeviceInformation. + /// + /// [2.5.10]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/616b66d5-b335-4e1c-8f87-b4a55e8d3e4a + #[derive(Debug, PartialEq, Clone)] + pub struct Characteristics: u32 { + const FILE_REMOVABLE_MEDIA = 0x00000001; + const FILE_READ_ONLY_DEVICE = 0x00000002; + const FILE_FLOPPY_DISKETTE = 0x00000004; + const FILE_WRITE_ONCE_MEDIA = 0x00000008; + const FILE_REMOTE_DEVICE = 0x00000010; + const FILE_DEVICE_IS_MOUNTED = 0x00000020; + const FILE_VIRTUAL_VOLUME = 0x00000040; + const FILE_DEVICE_SECURE_OPEN = 0x00000100; + const FILE_CHARACTERISTIC_TS_DEVICE = 0x00001000; + const FILE_CHARACTERISTIC_WEBDAV_DEVICE = 0x00002000; + const FILE_DEVICE_ALLOW_APPCONTAINER_TRAVERSAL = 0x00020000; + const FILE_PORTABLE_DEVICE = 0x0004000; + } +} + +/// [2.2.3.4.6] Client Drive Query Volume Information Response (DR_DRIVE_QUERY_VOLUME_INFORMATION_RSP) +/// +/// [2.2.3.4.6]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/fbdc7db8-a268-4420-8b5e-ce689ad1d4ac +#[derive(Debug, PartialEq, Clone)] +pub struct ClientDriveQueryVolumeInformationResponse { + pub device_io_reply: DeviceIoResponse, + pub buffer: Option, +} + +impl ClientDriveQueryVolumeInformationResponse { + const NAME: &'static str = "DR_DRIVE_QUERY_VOLUME_INFORMATION_RSP"; + + pub fn new( + device_io_request: DeviceIoRequest, + io_status: NtStatus, + buffer: Option, + ) -> Self { + Self { + device_io_reply: DeviceIoResponse::new(device_io_request, io_status), + buffer, + } + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.device_io_reply.encode(dst)?; + dst.write_u32(cast_length!( + "ClientDriveQueryVolumeInformationResponse", + "length", + self.buffer.as_ref().map_or(0, |buf| buf.size()) + )?); + if let Some(buffer) = &self.buffer { + buffer.encode(dst)?; + } + + Ok(()) + } + + pub fn size(&self) -> usize { + self.device_io_reply.size() // DeviceIoResponse + + 4 // Length + + if let Some(buffer) = &self.buffer { + buffer.size() // Buffer + } else { + 0 + } + } +} + +/// [2.2.1.4.3] Device Read Request (DR_READ_REQ) +/// +/// [2.2.1.4.3]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/3192516d-36a6-47c5-987a-55c214aa0441 +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceReadRequest { + pub device_io_request: DeviceIoRequest, + pub length: u32, + pub offset: u64, +} + +impl DeviceReadRequest { + const FIXED_PART_SIZE: usize = 4 /* Length */ + 8 /* Offset */ + 20 /* Padding */; + + pub fn decode(dev_io_req: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let length = src.read_u32(); + let offset = src.read_u64(); + // Padding (20 bytes): An array of 20 bytes. Reserved. This field can be set to any value and MUST be ignored. + read_padding!(src, 20); + + Ok(Self { + device_io_request: dev_io_req, + length, + offset, + }) + } +} + +/// [2.2.1.5.3] Device Read Response (DR_READ_RSP) +/// +/// [2.2.1.5.3]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/d35d3f91-fc5b-492b-80be-47f483ad1dc9 +pub struct DeviceReadResponse { + pub device_io_reply: DeviceIoResponse, + pub read_data: Vec, +} + +impl DeviceReadResponse { + const NAME: &'static str = "DR_READ_RSP"; + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.device_io_reply.encode(dst)?; + dst.write_u32(cast_length!("DeviceReadResponse", "length", self.read_data.len())?); + dst.write_slice(&self.read_data); + Ok(()) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + self.device_io_reply.size() // DeviceIoResponse + + 4 // Length + + self.read_data.len() // ReadData + } +} + +impl Debug for DeviceReadResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DeviceReadResponse") + .field("device_io_reply", &self.device_io_reply) + .field("read_data", &format!("Vec of length {}", self.read_data.len())) + .finish() + } +} + +/// [2.2.1.4.4] Device Write Request (DR_WRITE_REQ) +/// +/// [2.2.1.4.4]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/2e25f0aa-a4ce-4ff3-ad62-ab6098280a3a +#[derive(PartialEq, Clone)] +pub struct DeviceWriteRequest { + pub device_io_request: DeviceIoRequest, + pub offset: u64, + pub write_data: Vec, +} + +impl DeviceWriteRequest { + const FIXED_PART_SIZE: usize = 4 /* Length */ + 8 /* Offset */ + 20 /* Padding */; + + pub fn decode(dev_io_req: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let length = cast_length!("DeviceWriteRequest", "length", src.read_u32())?; + let offset = src.read_u64(); + // Padding (20 bytes): An array of 20 bytes. Reserved. This field can be set to any value and MUST be ignored. + read_padding!(src, 20); + + ensure_size!(in: src, size: length); + let write_data = src.read_slice(length).to_vec(); + + Ok(Self { + device_io_request: dev_io_req, + offset, + write_data, + }) + } +} + +impl Debug for DeviceWriteRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DeviceWriteRequest") + .field("device_io_request", &self.device_io_request) + .field("offset", &self.offset) + .field("write_data", &format!("Vec of length {}", self.write_data.len())) + .finish() + } +} + +/// [2.2.1.5.4] Device Write Response (DR_WRITE_RSP) +/// +/// [2.2.1.5.4]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/58160a47-2379-4c4a-a99d-24a1a666c02a +#[derive(Debug, PartialEq, Clone)] +pub struct DeviceWriteResponse { + pub device_io_reply: DeviceIoResponse, + pub length: u32, +} + +impl DeviceWriteResponse { + const NAME: &'static str = "DR_WRITE_RSP"; + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.device_io_reply.encode(dst)?; + dst.write_u32(self.length); + write_padding!(dst, 1); // Padding + Ok(()) + } + + pub fn size(&self) -> usize { + self.device_io_reply.size() // DeviceIoResponse + + 4 // Length + + 1 // Padding + } +} + +/// [2.2.3.3.9] Server Drive Set Information Request (DR_DRIVE_SET_INFORMATION_REQ) +/// +/// [2.2.3.3.9]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/b5d3104b-0e42-4cf8-9059-e9fe86615e5c +#[derive(Debug, PartialEq, Clone)] +pub struct ServerDriveSetInformationRequest { + pub device_io_request: DeviceIoRequest, + pub set_buffer: FileInformationClass, +} + +impl ServerDriveSetInformationRequest { + const FIXED_PART_SIZE: usize = 4 /* FileInformationClass */ + 4 /* Length */ + 24 /* Padding */; + + fn decode(dev_io_req: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let file_information_class_level = FileInformationClassLevel::from(src.read_u32()); + + // This field MUST contain one of the following values. + match file_information_class_level { + FileInformationClassLevel::FILE_BASIC_INFORMATION + | FileInformationClassLevel::FILE_END_OF_FILE_INFORMATION + | FileInformationClassLevel::FILE_DISPOSITION_INFORMATION + | FileInformationClassLevel::FILE_RENAME_INFORMATION + | FileInformationClassLevel::FILE_ALLOCATION_INFORMATION => {} + _ => { + return Err(invalid_field_err!( + "ServerDriveSetInformationRequest::decode", + "file_information_class_level", + "received invalid level" + )) + } + }; + + let length = cast_length!("ServerDriveSetInformationRequest", "length", src.read_u32())?; + + read_padding!(src, 24); // Padding + + let set_buffer = FileInformationClass::decode(file_information_class_level, length, src)?; + + Ok(Self { + device_io_request: dev_io_req, + set_buffer, + }) + } +} + +/// 2.4.13 FileEndOfFileInformation +/// +/// [2.4.13]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/75241cca-3167-472f-8058-a52d77c6bb17 +#[derive(Debug, PartialEq, Clone)] +pub struct FileEndOfFileInformation { + pub end_of_file: i64, +} + +impl FileEndOfFileInformation { + const FIXED_PART_SIZE: usize = 8; // EndOfFile + + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let end_of_file = src.read_i64(); + Ok(Self { end_of_file }) + } + + fn size() -> usize { + Self::FIXED_PART_SIZE + } +} + +/// [2.4.11] FileDispositionInformation +/// +/// [2.4.11]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/12c3dd1c-14f6-4229-9d29-75fb2cb392f6 +#[derive(Debug, PartialEq, Clone)] +pub struct FileDispositionInformation { + pub delete_pending: u8, +} + +impl FileDispositionInformation { + const FIXED_PART_SIZE: usize = 1; // DeletePending + + fn decode(src: &mut ReadCursor<'_>, length: usize) -> DecodeResult { + // https://github.com/FreeRDP/FreeRDP/blob/dfa231c0a55b005af775b833f92f6bcd30363d77/channels/drive/client/drive_file.c#L684-L692 + let delete_pending = if length != 0 { + ensure_fixed_part_size!(in: src); + src.read_u8() + } else { + 1 + }; + Ok(Self { delete_pending }) + } + + fn size() -> usize { + Self::FIXED_PART_SIZE + } +} + +/// [2.4.37] FileRenameInformation +/// +/// [2.4.37]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/1d2673a8-8fb9-4868-920a-775ccaa30cf8 +#[derive(Debug, PartialEq, Clone)] +pub struct FileRenameInformation { + pub replace_if_exists: Boolean, + /// `file_name` is the relative path to the new location of the file + pub file_name: String, +} + +impl FileRenameInformation { + const FIXED_PART_SIZE: usize = 1 /* ReplaceIfExists */ + 1 /* RootDirectory */ + 4 /* FileNameLength */; + + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let replace_if_exists = Boolean::from(src.read_u8()); + let _ = src.read_u8(); // RootDirectory + let file_name_length = cast_length!("FileRenameInformation", "file_name_length", src.read_u32())?; + + ensure_size!(in: src, size: file_name_length); + let file_name = decode_string(src.read_slice(file_name_length), CharacterSet::Unicode, true)?; + + Ok(Self { + replace_if_exists, + file_name, + }) + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + encoded_str_len(&self.file_name, CharacterSet::Unicode, true) + } +} + +/// [2.4.4] FileAllocationInformation +/// +/// [2.4.4]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/0201c69b-50db-412d-bab3-dd97aeede13b +#[derive(Debug, PartialEq, Clone)] +pub struct FileAllocationInformation { + pub allocation_size: i64, +} + +impl FileAllocationInformation { + const FIXED_PART_SIZE: usize = 8; // AllocationSize + + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + let allocation_size = src.read_i64(); + Ok(Self { allocation_size }) + } + + fn size() -> usize { + Self::FIXED_PART_SIZE + } +} + +/// [2.2.3.4.9] Client Drive Set Information Response (DR_DRIVE_SET_INFORMATION_RSP) +/// +/// [2.2.3.4.9]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/16b893d5-5d8b-49d1-8dcb-ee21e7612970 +#[derive(Debug, PartialEq, Clone)] +pub struct ClientDriveSetInformationResponse { + device_io_reply: DeviceIoResponse, + /// This field MUST be equal to the Length field in the Server Drive Set Information Request (section 2.2.3.3.9). + length: u32, +} + +impl ClientDriveSetInformationResponse { + const NAME: &'static str = "DR_DRIVE_SET_INFORMATION_RSP"; + + pub fn new(req: &ServerDriveSetInformationRequest, io_status: NtStatus) -> EncodeResult { + Ok(Self { + device_io_reply: DeviceIoResponse::new(req.device_io_request.clone(), io_status), + length: cast_length!("ClientDriveSetInformationResponse", "length", req.set_buffer.size())?, + }) + } + + pub fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.device_io_reply.encode(dst)?; + dst.write_u32(self.length); + Ok(()) + } + + pub fn name(&self) -> &'static str { + Self::NAME + } + + pub fn size(&self) -> usize { + self.device_io_reply.size() // DeviceIoResponse + + 4 // Length + } +} + +/// 2.2.3.3.12 Server Drive Lock Control Request (DR_DRIVE_LOCK_REQ) +/// +/// [2.2.3.3.12]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a96fe85c-620c-40ce-8858-a6bc38609b0a +#[derive(Debug, PartialEq, Clone)] +pub struct ServerDriveLockControlRequest { + pub device_io_request: DeviceIoRequest, +} + +impl ServerDriveLockControlRequest { + fn decode(dev_io_req: DeviceIoRequest, src: &mut ReadCursor<'_>) -> DecodeResult { + // It's not quite clear why this is done this way, but it's what FreeRDP does: + // https://github.com/FreeRDP/FreeRDP/blob/dfa231c0a55b005af775b833f92f6bcd30363d77/channels/drive/client/drive_main.c#L600 + ensure_size!(in: src, size: 4); + let _ = src.read_u32(); + Ok(Self { + device_io_request: dev_io_req, + }) + } +} diff --git a/crates/ironrdp-rdpdr/src/pdu/esc/mod.rs b/crates/ironrdp-rdpdr/src/pdu/esc/mod.rs new file mode 100644 index 00000000..1b495a7b --- /dev/null +++ b/crates/ironrdp-rdpdr/src/pdu/esc/mod.rs @@ -0,0 +1,1859 @@ +//! PDUs for [\[MS-RDPESC\]: Remote Desktop Protocol: Smart Card Virtual Channel Extension] +//! +//! [\[MS-RDPESC\]: Remote Desktop Protocol: Smart Card Virtual Channel Extension]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/0428ca28-b4dc-46a3-97c3-01887fa44a90 + +pub mod ndr; +pub mod rpce; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_size, invalid_field_err, other_err, DecodeError, DecodeResult, EncodeResult, ReadCursor, + WriteCursor, +}; +use ironrdp_pdu::utils::{ + encoded_multistring_len, read_multistring_from_cursor, write_multistring_to_cursor, CharacterSet, +}; +use tracing::{error, warn}; + +use super::efs::IoCtlCode; +use crate::pdu::esc::ndr::{Decode as _, Encode as _}; + +/// [2.2.2] TS Server-Generated Structures +/// +/// [2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/f4ca3b61-b49c-463c-8932-2cf82fb7ec7a +#[derive(Debug, PartialEq, Clone)] +pub enum ScardCall { + AccessStartedEventCall(ScardAccessStartedEventCall), + EstablishContextCall(EstablishContextCall), + ListReadersCall(ListReadersCall), + GetStatusChangeCall(GetStatusChangeCall), + ConnectCall(ConnectCall), + HCardAndDispositionCall(HCardAndDispositionCall), + TransmitCall(TransmitCall), + StatusCall(StatusCall), + ContextCall(ContextCall), + GetDeviceTypeIdCall(GetDeviceTypeIdCall), + ReadCacheCall(ReadCacheCall), + WriteCacheCall(WriteCacheCall), + GetReaderIconCall(GetReaderIconCall), + Unsupported, +} + +impl ScardCall { + pub fn decode(io_ctl_code: ScardIoCtlCode, src: &mut ReadCursor<'_>) -> DecodeResult { + match io_ctl_code { + ScardIoCtlCode::AccessStartedEvent => Ok(ScardCall::AccessStartedEventCall( + ScardAccessStartedEventCall::decode(src)?, + )), + ScardIoCtlCode::EstablishContext => Ok(ScardCall::EstablishContextCall(EstablishContextCall::decode(src)?)), + ScardIoCtlCode::ListReadersW => Ok(ScardCall::ListReadersCall(ListReadersCall::decode( + src, + Some(CharacterSet::Unicode), + )?)), + ScardIoCtlCode::ListReadersA => Ok(ScardCall::ListReadersCall(ListReadersCall::decode( + src, + Some(CharacterSet::Ansi), + )?)), + ScardIoCtlCode::GetStatusChangeW => Ok(ScardCall::GetStatusChangeCall(GetStatusChangeCall::decode( + src, + Some(CharacterSet::Unicode), + )?)), + ScardIoCtlCode::GetStatusChangeA => Ok(ScardCall::GetStatusChangeCall(GetStatusChangeCall::decode( + src, + Some(CharacterSet::Ansi), + )?)), + ScardIoCtlCode::ConnectW => Ok(ScardCall::ConnectCall(ConnectCall::decode( + src, + Some(CharacterSet::Unicode), + )?)), + ScardIoCtlCode::ConnectA => Ok(ScardCall::ConnectCall(ConnectCall::decode( + src, + Some(CharacterSet::Ansi), + )?)), + ScardIoCtlCode::BeginTransaction => Ok(ScardCall::HCardAndDispositionCall( + HCardAndDispositionCall::decode(src)?, + )), + ScardIoCtlCode::Transmit => Ok(ScardCall::TransmitCall(TransmitCall::decode(src)?)), + ScardIoCtlCode::StatusW | ScardIoCtlCode::StatusA => Ok(ScardCall::StatusCall(StatusCall::decode(src)?)), + ScardIoCtlCode::ReleaseContext => Ok(ScardCall::ContextCall(ContextCall::decode(src)?)), + ScardIoCtlCode::EndTransaction => Ok(ScardCall::HCardAndDispositionCall(HCardAndDispositionCall::decode( + src, + )?)), + ScardIoCtlCode::Disconnect => Ok(ScardCall::HCardAndDispositionCall(HCardAndDispositionCall::decode( + src, + )?)), + ScardIoCtlCode::Cancel => Ok(ScardCall::ContextCall(ContextCall::decode(src)?)), + ScardIoCtlCode::IsValidContext => Ok(ScardCall::ContextCall(ContextCall::decode(src)?)), + ScardIoCtlCode::GetDeviceTypeId => Ok(ScardCall::GetDeviceTypeIdCall(GetDeviceTypeIdCall::decode(src)?)), + ScardIoCtlCode::ReadCacheW => Ok(ScardCall::ReadCacheCall(ReadCacheCall::decode( + src, + Some(CharacterSet::Unicode), + )?)), + ScardIoCtlCode::ReadCacheA => Ok(ScardCall::ReadCacheCall(ReadCacheCall::decode( + src, + Some(CharacterSet::Ansi), + )?)), + ScardIoCtlCode::WriteCacheW => Ok(ScardCall::WriteCacheCall(WriteCacheCall::decode( + src, + Some(CharacterSet::Unicode), + )?)), + ScardIoCtlCode::WriteCacheA => Ok(ScardCall::WriteCacheCall(WriteCacheCall::decode( + src, + Some(CharacterSet::Ansi), + )?)), + ScardIoCtlCode::GetReaderIcon => Ok(ScardCall::GetReaderIconCall(GetReaderIconCall::decode(src)?)), + _ => { + warn!(?io_ctl_code, "Unsupported ScardIoCtlCode"); + // TODO: maybe this should be an error + Ok(Self::Unsupported) + } + } + } +} + +/// [2.2.1.1] REDIR_SCARDCONTEXT +/// +/// [2.2.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/060abee1-e520-4149-9ef7-ce79eb500a59 +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct ScardContext { + /// Shortcut: we always create 4-byte context values. + /// The spec allows this field to have variable length. + pub value: u32, +} + +impl ScardContext { + /// See [`ScardContext::value`] + const VALUE_LENGTH: u32 = 4; + + pub fn new(value: u32) -> Self { + Self { value } + } +} + +impl ndr::Encode for ScardContext { + fn encode_ptr(&self, index: &mut u32, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ndr::encode_ptr(Some(Self::VALUE_LENGTH), index, dst) + } + + fn encode_value(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size_value()); + dst.write_u32(Self::VALUE_LENGTH); + dst.write_u32(self.value); + Ok(()) + } + + fn size_ptr(&self) -> usize { + ndr::ptr_size(true) + } + + fn size_value(&self) -> usize { + 4 /* cbContext */ + 4 /* pbContext */ + } +} + +impl ndr::Decode for ScardContext { + fn decode_ptr(src: &mut ReadCursor<'_>, index: &mut u32) -> DecodeResult + where + Self: Sized, + { + ensure_size!(in: src, size: size_of::()); + let length = src.read_u32(); + if length != Self::VALUE_LENGTH { + error!(?length, "Unsupported value length in ScardContext"); + return Err(invalid_field_err!( + "decode_ptr", + "unsupported value length in ScardContext" + )); + } + + let _ptr = ndr::decode_ptr(src, index)?; + Ok(Self { value: 0 }) + } + + fn decode_value(&mut self, src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult<()> { + expect_no_charset(charset)?; + ensure_size!(in: src, size: size_of::() * 2); + let length = src.read_u32(); + if length != Self::VALUE_LENGTH { + error!(?length, "Unsupported value length in ScardContext"); + return Err(invalid_field_err!( + "decode_value", + "unsupported value length in ScardContext" + )); + } + self.value = src.read_u32(); + Ok(()) + } +} + +/// [2.2.1.7] ReaderStateW +/// +/// [2.2.1.7]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/0ba03cd2-bed0-495b-adbe-3d2cde61980c +#[derive(Debug, PartialEq, Clone)] +pub struct ReaderState { + pub reader: String, + pub common: ReaderStateCommonCall, +} + +impl ndr::Decode for ReaderState { + fn decode_ptr(src: &mut ReadCursor<'_>, index: &mut u32) -> DecodeResult { + let _reader_ptr = ndr::decode_ptr(src, index)?; + let common = ReaderStateCommonCall::decode(src)?; + Ok(Self { + reader: String::new(), + common, + }) + } + + fn decode_value(&mut self, src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult<()> { + let charset = expect_charset(charset)?; + self.reader = ndr::read_string_from_cursor(src, charset)?; + Ok(()) + } +} + +/// From [3.1.4] Message Processing Events and Sequencing Rules +/// +/// [3.1.4]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/60d5977d-0017-4c90-ab0c-f34bf44a74a5 +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u32)] +pub enum ScardIoCtlCode { + /// SCARD_IOCTL_ESTABLISHCONTEXT + EstablishContext = 0x0009_0014, + /// SCARD_IOCTL_RELEASECONTEXT + ReleaseContext = 0x0009_0018, + /// SCARD_IOCTL_ISVALIDCONTEXT + IsValidContext = 0x0009_001C, + /// SCARD_IOCTL_LISTREADERGROUPSA + ListReaderGroupsA = 0x0009_0020, + /// SCARD_IOCTL_LISTREADERGROUPSW + ListReaderGroupsW = 0x0009_0024, + /// SCARD_IOCTL_LISTREADERSA + ListReadersA = 0x0009_0028, + /// SCARD_IOCTL_LISTREADERSW + ListReadersW = 0x0009_002C, + /// SCARD_IOCTL_INTRODUCEREADERGROUPA + IntroduceReaderGroupA = 0x0009_0050, + /// SCARD_IOCTL_INTRODUCEREADERGROUPW + IntroduceReaderGroupW = 0x0009_0054, + /// SCARD_IOCTL_FORGETREADERGROUPA + ForgetReaderGroupA = 0x0009_0058, + /// SCARD_IOCTL_FORGETREADERGROUPW + ForgetReaderGroupW = 0x0009_005C, + /// SCARD_IOCTL_INTRODUCEREADERA + IntroduceReaderA = 0x0009_0060, + /// SCARD_IOCTL_INTRODUCEREADERW + IntroduceReaderW = 0x0009_0064, + /// SCARD_IOCTL_FORGETREADERA + ForgetReaderA = 0x0009_0068, + /// SCARD_IOCTL_FORGETREADERW + ForgetReaderW = 0x0009_006C, + /// SCARD_IOCTL_ADDREADERTOGROUPA + AddReaderToGroupA = 0x0009_0070, + /// SCARD_IOCTL_ADDREADERTOGROUPW + AddReaderToGroupW = 0x0009_0074, + /// SCARD_IOCTL_REMOVEREADERFROMGROUPA + RemoveReaderFromGroupA = 0x0009_0078, + /// SCARD_IOCTL_REMOVEREADERFROMGROUPW + RemoveReaderFromGroupW = 0x0009_007C, + /// SCARD_IOCTL_LOCATECARDSA + LocateCardsA = 0x0009_0098, + /// SCARD_IOCTL_LOCATECARDSW + LocateCardsW = 0x0009_009C, + /// SCARD_IOCTL_GETSTATUSCHANGEA + GetStatusChangeA = 0x0009_00A0, + /// SCARD_IOCTL_GETSTATUSCHANGEW + GetStatusChangeW = 0x0009_00A4, + /// SCARD_IOCTL_CANCEL + Cancel = 0x0009_00A8, + /// SCARD_IOCTL_CONNECTA + ConnectA = 0x0009_00AC, + /// SCARD_IOCTL_CONNECTW + ConnectW = 0x0009_00B0, + /// SCARD_IOCTL_RECONNECT + Reconnect = 0x0009_00B4, + /// SCARD_IOCTL_DISCONNECT + Disconnect = 0x0009_00B8, + /// SCARD_IOCTL_BEGINTRANSACTION + BeginTransaction = 0x0009_00BC, + /// SCARD_IOCTL_ENDTRANSACTION + EndTransaction = 0x0009_00C0, + /// SCARD_IOCTL_STATE + State = 0x0009_00C4, + /// SCARD_IOCTL_STATUSA + StatusA = 0x0009_00C8, + /// SCARD_IOCTL_STATUSW + StatusW = 0x0009_00CC, + /// SCARD_IOCTL_TRANSMIT + Transmit = 0x0009_00D0, + /// SCARD_IOCTL_CONTROL + Control = 0x0009_00D4, + /// SCARD_IOCTL_GETATTRIB + GetAttrib = 0x0009_00D8, + /// SCARD_IOCTL_SETATTRIB + SetAttrib = 0x0009_00DC, + /// SCARD_IOCTL_ACCESSSTARTEDEVENT + AccessStartedEvent = 0x0009_00E0, + /// SCARD_IOCTL_RELEASETARTEDEVENT + ReleaseTartedEvent = 0x0009_00E4, + /// SCARD_IOCTL_LOCATECARDSBYATRA + LocateCardsByAtrA = 0x0009_00E8, + /// SCARD_IOCTL_LOCATECARDSBYATRW + LocateCardsByAtrW = 0x0009_00EC, + /// SCARD_IOCTL_READCACHEA + ReadCacheA = 0x0009_00F0, + /// SCARD_IOCTL_READCACHEW + ReadCacheW = 0x0009_00F4, + /// SCARD_IOCTL_WRITECACHEA + WriteCacheA = 0x0009_00F8, + /// SCARD_IOCTL_WRITECACHEW + WriteCacheW = 0x0009_00FC, + /// SCARD_IOCTL_GETTRANSMITCOUNT + GetTransmitCount = 0x0009_0100, + /// SCARD_IOCTL_GETREADERICON + GetReaderIcon = 0x0009_0104, + /// SCARD_IOCTL_GETDEVICETYPEID + GetDeviceTypeId = 0x0009_0108, +} + +impl TryFrom for ScardIoCtlCode { + type Error = DecodeError; + + fn try_from(value: u32) -> Result { + match value { + 0x0009_0014 => Ok(ScardIoCtlCode::EstablishContext), + 0x0009_0018 => Ok(ScardIoCtlCode::ReleaseContext), + 0x0009_001C => Ok(ScardIoCtlCode::IsValidContext), + 0x0009_0020 => Ok(ScardIoCtlCode::ListReaderGroupsA), + 0x0009_0024 => Ok(ScardIoCtlCode::ListReaderGroupsW), + 0x0009_0028 => Ok(ScardIoCtlCode::ListReadersA), + 0x0009_002C => Ok(ScardIoCtlCode::ListReadersW), + 0x0009_0050 => Ok(ScardIoCtlCode::IntroduceReaderGroupA), + 0x0009_0054 => Ok(ScardIoCtlCode::IntroduceReaderGroupW), + 0x0009_0058 => Ok(ScardIoCtlCode::ForgetReaderGroupA), + 0x0009_005C => Ok(ScardIoCtlCode::ForgetReaderGroupW), + 0x0009_0060 => Ok(ScardIoCtlCode::IntroduceReaderA), + 0x0009_0064 => Ok(ScardIoCtlCode::IntroduceReaderW), + 0x0009_0068 => Ok(ScardIoCtlCode::ForgetReaderA), + 0x0009_006C => Ok(ScardIoCtlCode::ForgetReaderW), + 0x0009_0070 => Ok(ScardIoCtlCode::AddReaderToGroupA), + 0x0009_0074 => Ok(ScardIoCtlCode::AddReaderToGroupW), + 0x0009_0078 => Ok(ScardIoCtlCode::RemoveReaderFromGroupA), + 0x0009_007C => Ok(ScardIoCtlCode::RemoveReaderFromGroupW), + 0x0009_0098 => Ok(ScardIoCtlCode::LocateCardsA), + 0x0009_009C => Ok(ScardIoCtlCode::LocateCardsW), + 0x0009_00A0 => Ok(ScardIoCtlCode::GetStatusChangeA), + 0x0009_00A4 => Ok(ScardIoCtlCode::GetStatusChangeW), + 0x0009_00A8 => Ok(ScardIoCtlCode::Cancel), + 0x0009_00AC => Ok(ScardIoCtlCode::ConnectA), + 0x0009_00B0 => Ok(ScardIoCtlCode::ConnectW), + 0x0009_00B4 => Ok(ScardIoCtlCode::Reconnect), + 0x0009_00B8 => Ok(ScardIoCtlCode::Disconnect), + 0x0009_00BC => Ok(ScardIoCtlCode::BeginTransaction), + 0x0009_00C0 => Ok(ScardIoCtlCode::EndTransaction), + 0x0009_00C4 => Ok(ScardIoCtlCode::State), + 0x0009_00C8 => Ok(ScardIoCtlCode::StatusA), + 0x0009_00CC => Ok(ScardIoCtlCode::StatusW), + 0x0009_00D0 => Ok(ScardIoCtlCode::Transmit), + 0x0009_00D4 => Ok(ScardIoCtlCode::Control), + 0x0009_00D8 => Ok(ScardIoCtlCode::GetAttrib), + 0x0009_00DC => Ok(ScardIoCtlCode::SetAttrib), + 0x0009_00E0 => Ok(ScardIoCtlCode::AccessStartedEvent), + 0x0009_00E4 => Ok(ScardIoCtlCode::ReleaseTartedEvent), + 0x0009_00E8 => Ok(ScardIoCtlCode::LocateCardsByAtrA), + 0x0009_00EC => Ok(ScardIoCtlCode::LocateCardsByAtrW), + 0x0009_00F0 => Ok(ScardIoCtlCode::ReadCacheA), + 0x0009_00F4 => Ok(ScardIoCtlCode::ReadCacheW), + 0x0009_00F8 => Ok(ScardIoCtlCode::WriteCacheA), + 0x0009_00FC => Ok(ScardIoCtlCode::WriteCacheW), + 0x0009_0100 => Ok(ScardIoCtlCode::GetTransmitCount), + 0x0009_0104 => Ok(ScardIoCtlCode::GetReaderIcon), + 0x0009_0108 => Ok(ScardIoCtlCode::GetDeviceTypeId), + _ => { + error!("Unsupported ScardIoCtlCode: 0x{:08x}", value); + Err(invalid_field_err!("try_from", "ScardIoCtlCode", "unsupported value")) + } + } + } +} + +/// Allow [`ScardIoCtlCode`] to be used as an [`IoCtlCode`]. +impl IoCtlCode for ScardIoCtlCode {} + +/// [2.2.2.30] ScardAccessStartedEvent_Call +/// +/// [2.2.2.30]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/c5ab8dd0-4914-4355-960c-0a527971ea69 +#[derive(Debug, PartialEq, Clone)] +pub struct ScardAccessStartedEventCall; + +impl ScardAccessStartedEventCall { + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ironrdp_pdu::read_padding!(src, 4); // Unused (4 bytes) + Ok(Self) + } +} + +/// [2.2.3.3] Long_Return +/// +/// [2.2.3.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/e77a1365-2379-4037-99c4-d30d14ba10fc +#[derive(Debug, PartialEq, Clone)] +pub struct LongReturn { + return_code: ReturnCode, +} + +impl LongReturn { + const NAME: &'static str = "Long_Return"; + + pub fn new(return_code: ReturnCode) -> rpce::Pdu { + rpce::Pdu(Self { return_code }) + } +} + +impl rpce::HeaderlessEncode for LongReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.return_code.size() + } +} + +/// [2.2.8] Return Code +/// +/// [2.2.8]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/9861f8da-76fe-41e6-847e-40c9aa35df8d +#[derive(Debug, PartialEq, Clone, Copy)] +#[repr(u32)] +pub enum ReturnCode { + /// SCARD_S_SUCCESS + Success = 0x0000_0000, + /// SCARD_F_INTERNAL_ERROR + InternalError = 0x8010_0001, + /// SCARD_E_CANCELLED + Cancelled = 0x8010_0002, + /// SCARD_E_INVALID_HANDLE + InvalidHandle = 0x8010_0003, + /// SCARD_E_INVALID_PARAMETER + InvalidParameter = 0x8010_0004, + /// SCARD_E_INVALID_TARGET + InvalidTarget = 0x8010_0005, + /// SCARD_E_NO_MEMORY + NoMemory = 0x8010_0006, + /// SCARD_F_WAITED_TOO_LONG + WaitedTooLong = 0x8010_0007, + /// SCARD_E_INSUFFICIENT_BUFFER + InsufficientBuffer = 0x8010_0008, + /// SCARD_E_UNKNOWN_READER + UnknownReader = 0x8010_0009, + /// SCARD_E_TIMEOUT + Timeout = 0x8010_000A, + /// SCARD_E_SHARING_VIOLATION + SharingViolation = 0x8010_000B, + /// SCARD_E_NO_SMARTCARD + NoSmartcard = 0x8010_000C, + /// SCARD_E_UNKNOWN_CARD + UnknownCard = 0x8010_000D, + /// SCARD_E_CANT_DISPOSE + CantDispose = 0x8010_000E, + /// SCARD_E_PROTO_MISMATCH + ProtoMismatch = 0x8010_000F, + /// SCARD_E_NOT_READY + NotReady = 0x8010_0010, + /// SCARD_E_INVALID_VALUE + InvalidValue = 0x8010_0011, + /// SCARD_E_SYSTEM_CANCELLED + SystemCancelled = 0x8010_0012, + /// SCARD_F_COMM_ERROR + CommError = 0x8010_0013, + /// SCARD_F_UNKNOWN_ERROR + UnknownError = 0x8010_0014, + /// SCARD_E_INVALID_ATR + InvalidAtr = 0x8010_0015, + /// SCARD_E_NOT_TRANSACTED + NotTransacted = 0x8010_0016, + /// SCARD_E_READER_UNAVAILABLE + ReaderUnavailable = 0x8010_0017, + /// SCARD_P_SHUTDOWN + Shutdown = 0x8010_0018, + /// SCARD_E_PCI_TOO_SMALL + PciTooSmall = 0x8010_0019, + /// SCARD_E_ICC_INSTALLATION + IccInstallation = 0x8010_0020, + /// SCARD_E_ICC_CREATEORDER + IccCreateorder = 0x8010_0021, + /// SCARD_E_UNSUPPORTED_FEATURE + UnsupportedFeature = 0x8010_0022, + /// SCARD_E_DIR_NOT_FOUND + DirNotFound = 0x8010_0023, + /// SCARD_E_FILE_NOT_FOUND + FileNotFound = 0x8010_0024, + /// SCARD_E_NO_DIR + NoDir = 0x8010_0025, + /// SCARD_E_READER_UNSUPPORTED + ReaderUnsupported = 0x8010_001A, + /// SCARD_E_DUPLICATE_READER + DuplicateReader = 0x8010_001B, + /// SCARD_E_CARD_UNSUPPORTED + CardUnsupported = 0x8010_001C, + /// SCARD_E_NO_SERVICE + NoService = 0x8010_001D, + /// SCARD_E_SERVICE_STOPPED + ServiceStopped = 0x8010_001E, + /// SCARD_E_UNEXPECTED + Unexpected = 0x8010_001F, + /// SCARD_E_NO_FILE + NoFile = 0x8010_0026, + /// SCARD_E_NO_ACCESS + NoAccess = 0x8010_0027, + /// SCARD_E_WRITE_TOO_MANY + WriteTooMany = 0x8010_0028, + /// SCARD_E_BAD_SEEK + BadSeek = 0x8010_0029, + /// SCARD_E_INVALID_CHV + InvalidChv = 0x8010_002A, + /// SCARD_E_UNKNOWN_RES_MSG + UnknownResMsg = 0x8010_002B, + /// SCARD_E_NO_SUCH_CERTIFICATE + NoSuchCertificate = 0x8010_002C, + /// SCARD_E_CERTIFICATE_UNAVAILABLE + CertificateUnavailable = 0x8010_002D, + /// SCARD_E_NO_READERS_AVAILABLE + NoReadersAvailable = 0x8010_002E, + /// SCARD_E_COMM_DATA_LOST + CommDataLost = 0x8010_002F, + /// SCARD_E_NO_KEY_CONTAINER + NoKeyContainer = 0x8010_0030, + /// SCARD_E_SERVER_TOO_BUSY + ServerTooBusy = 0x8010_0031, + /// SCARD_E_PIN_CACHE_EXPIRED + PinCacheExpired = 0x8010_0032, + /// SCARD_E_NO_PIN_CACHE + NoPinCache = 0x8010_0033, + /// SCARD_E_READ_ONLY_CARD + ReadOnlyCard = 0x8010_0034, + /// SCARD_W_UNSUPPORTED_CARD + UnsupportedCard = 0x8010_0065, + /// SCARD_W_UNRESPONSIVE_CARD + UnresponsiveCard = 0x8010_0066, + /// SCARD_W_UNPOWERED_CARD + UnpoweredCard = 0x8010_0067, + /// SCARD_W_RESET_CARD + ResetCard = 0x8010_0068, + /// SCARD_W_REMOVED_CARD + RemovedCard = 0x8010_0069, + /// SCARD_W_SECURITY_VIOLATION + SecurityViolation = 0x8010_006A, + /// SCARD_W_WRONG_CHV + WrongChv = 0x8010_006B, + /// SCARD_W_CHV_BLOCKED + ChvBlocked = 0x8010_006C, + /// SCARD_W_EOF + Eof = 0x8010_006D, + /// SCARD_W_CANCELLED_BY_USER + CancelledByUser = 0x8010_006E, + /// SCARD_W_CARD_NOT_AUTHENTICATED + CardNotAuthenticated = 0x8010_006F, + /// SCARD_W_CACHE_ITEM_NOT_FOUND + CacheItemNotFound = 0x8010_0070, + /// SCARD_W_CACHE_ITEM_STALE + CacheItemStale = 0x8010_0071, + /// SCARD_W_CACHE_ITEM_TOO_BIG + CacheItemTooBig = 0x8010_0072, +} + +impl ReturnCode { + pub fn size(&self) -> usize { + size_of::() + } +} + +impl From for u32 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(val: ReturnCode) -> Self { + val as u32 + } +} + +/// [2.2.2.1] EstablishContext_Call +/// +/// [2.2.2.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/b990635a-7637-464a-8923-361ed3e3d67a +#[derive(Debug, PartialEq, Clone)] +pub struct EstablishContextCall { + pub scope: Scope, +} + +impl EstablishContextCall { + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, None)?.into_inner()) + } + + fn size() -> usize { + size_of::() + } +} + +impl rpce::HeaderlessDecode for EstablishContextCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + expect_no_charset(charset)?; + ensure_size!(in: src, size: Self::size()); + let scope = Scope::try_from(src.read_u32())?; + Ok(Self { scope }) + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +#[repr(u32)] +pub enum Scope { + User = 0x0000_0000, + Terminal = 0x0000_0001, + System = 0x0000_0002, +} + +impl Scope { + pub fn size(&self) -> usize { + size_of::() + } +} + +impl TryFrom for Scope { + type Error = DecodeError; + + fn try_from(value: u32) -> Result { + match value { + 0x0000_0000 => Ok(Scope::User), + 0x0000_0001 => Ok(Scope::Terminal), + 0x0000_0002 => Ok(Scope::System), + _ => { + error!("Unsupported Scope: 0x{:08x}", value); + Err(invalid_field_err!("try_from", "Scope", "unsupported value")) + } + } + } +} + +/// [2.2.3.2] EstablishContext_Return +/// +/// [2.2.3.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/9135d95f-3740-411b-bdca-34ac7571fddc +#[derive(Debug, PartialEq, Clone)] +pub struct EstablishContextReturn { + return_code: ReturnCode, + context: ScardContext, +} + +impl EstablishContextReturn { + const NAME: &'static str = "EstablishContext_Return"; + + pub fn new(return_code: ReturnCode, context: ScardContext) -> rpce::Pdu { + rpce::Pdu(Self { return_code, context }) + } +} + +impl rpce::HeaderlessEncode for EstablishContextReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + let mut index = 0; + self.context.encode_ptr(&mut index, dst)?; + self.context.encode_value(dst)?; + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.return_code.size() + self.context.size() + } +} + +/// [2.2.2.4] ListReaders_Call +/// +/// [2.2.2.4]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/be2f46a5-77fb-40bf-839c-aed45f0a26d7 +#[derive(Debug, PartialEq, Clone)] +pub struct ListReadersCall { + pub context: ScardContext, + pub groups_ptr_length: u32, + pub groups_length: u32, + pub groups_ptr: u32, + pub groups: Vec, + pub readers_is_null: bool, // u32 + pub readers_size: u32, +} + +impl ListReadersCall { + pub fn decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, charset)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for ListReadersCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + let charset = expect_charset(charset)?; + let mut index = 0; + let mut context = ScardContext::decode_ptr(src, &mut index)?; + + ensure_size!(in: src, size: size_of::()); + let groups_ptr_length = src.read_u32(); + + let groups_ptr = ndr::decode_ptr(src, &mut index)?; + + ensure_size!(in: src, size: size_of::() * 2); + let readers_is_null = (src.read_u32()) == 0x0000_0001; + let readers_size = src.read_u32(); + + context.decode_value(src, None)?; + + if groups_ptr == 0 { + return Ok(Self { + context, + groups_ptr_length, + groups_ptr, + groups_length: 0, + groups: Vec::new(), + readers_is_null, + readers_size, + }); + } + + ensure_size!(in: src, size: size_of::()); + let groups_length = src.read_u32(); + if groups_length != groups_ptr_length { + return Err(invalid_field_err!( + "decode", + "mismatched reader groups length in NDR pointer and value" + )); + } + + let groups = read_multistring_from_cursor(src, charset)?; + + Ok(Self { + context, + groups_ptr_length, + groups_ptr, + groups_length, + groups, + readers_is_null, + readers_size, + }) + } +} + +/// [2.2.3.4] ListReaderGroups_Return and ListReaders_Return +/// +/// [2.2.3.4]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/6630bb5b-fc0e-4141-8b53-263225c7628d +#[derive(Debug, PartialEq, Clone)] +pub struct ListReadersReturn { + pub return_code: ReturnCode, + pub readers: Vec, +} + +impl ListReadersReturn { + const NAME: &'static str = "ListReaders_Return"; + + pub fn new(return_code: ReturnCode, readers: Vec) -> rpce::Pdu { + rpce::Pdu(Self { return_code, readers }) + } +} + +impl rpce::HeaderlessEncode for ListReadersReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + let readers_length: u32 = cast_length!( + "ListReadersReturn", + "readers", + encoded_multistring_len(&self.readers, CharacterSet::Unicode) + )?; + let mut index = 0; + ndr::encode_ptr(Some(readers_length), &mut index, dst)?; + dst.write_u32(readers_length); + write_multistring_to_cursor(dst, &self.readers, CharacterSet::Unicode)?; + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.return_code.size() // dst.write_u32(self.return_code.into()); + + ndr::ptr_size(true) // ndr::encode_ptr(...); + + 4 // dst.write_u32(readers_length); + + encoded_multistring_len(&self.readers, CharacterSet::Unicode) // write_multistring_to_cursor(...); + } +} + +/// [2.2.2.12] GetStatusChangeW_Call +/// +/// [2.2.2.12]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/af357ce8-63ee-4577-b6bf-c6f5ca68d754 +#[derive(Debug, PartialEq, Clone)] +pub struct GetStatusChangeCall { + pub context: ScardContext, + pub timeout: u32, + pub states_ptr_length: u32, + pub states_ptr: u32, + pub states_length: u32, + pub states: Vec, +} + +impl GetStatusChangeCall { + pub fn decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, charset)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for GetStatusChangeCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + let mut index = 0; + let mut context = ScardContext::decode_ptr(src, &mut index)?; + + ensure_size!(in: src, size: size_of::() * 2); + let timeout = src.read_u32(); + let states_ptr_length = src.read_u32(); + + let states_ptr = ndr::decode_ptr(src, &mut index)?; + + context.decode_value(src, None)?; + + ensure_size!(in: src, size: size_of::()); + let states_length = src.read_u32(); + + let mut states = Vec::new(); + for _ in 0..states_length { + let state = ReaderState::decode_ptr(src, &mut index)?; + states.push(state); + } + for state in states.iter_mut() { + state.decode_value(src, charset)?; + } + + Ok(Self { + context, + timeout, + states_ptr_length, + states_ptr, + states_length, + states, + }) + } +} + +/// [2.2.1.5] ReaderState_Common_Call +/// +/// [2.2.1.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/a71e63ba-e58f-487c-a5d2-5a3e48856594 +#[derive(Debug, PartialEq, Clone)] +pub struct ReaderStateCommonCall { + pub current_state: CardStateFlags, + pub event_state: CardStateFlags, + pub atr_length: u32, + pub atr: [u8; 36], +} + +impl ReaderStateCommonCall { + const FIXED_PART_SIZE: usize = size_of::() * 3 /* dwCurrentState, dwEventState, cbAtr */ + 36 /* rgbAtr */; + + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::FIXED_PART_SIZE); + let current_state = CardStateFlags::from_bits_retain(src.read_u32()); + let event_state = CardStateFlags::from_bits_retain(src.read_u32()); + let atr_length = src.read_u32(); + let atr = src.read_array::<36>(); + + Ok(Self { + current_state, + event_state, + atr_length, + atr, + }) + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + dst.write_u32(self.current_state.bits()); + dst.write_u32(self.event_state.bits()); + dst.write_u32(self.atr_length); + dst.write_slice(&self.atr); + Ok(()) + } + + fn size() -> usize { + Self::FIXED_PART_SIZE + } +} + +bitflags! { + #[derive(Debug, PartialEq, Clone, Copy)] + pub struct CardStateFlags: u32 { + const SCARD_STATE_UNAWARE = 0x0000_0000; + const SCARD_STATE_IGNORE = 0x0000_0001; + const SCARD_STATE_CHANGED = 0x0000_0002; + const SCARD_STATE_UNKNOWN = 0x0000_0004; + const SCARD_STATE_UNAVAILABLE = 0x0000_0008; + const SCARD_STATE_EMPTY = 0x0000_0010; + const SCARD_STATE_PRESENT = 0x0000_0020; + const SCARD_STATE_ATRMATCH = 0x0000_0040; + const SCARD_STATE_EXCLUSIVE = 0x0000_0080; + const SCARD_STATE_INUSE = 0x0000_0100; + const SCARD_STATE_MUTE = 0x0000_0200; + const SCARD_STATE_UNPOWERED = 0x0000_0400; + } +} + +/// [2.2.3.5] LocateCards_Return and GetStatusChange_Return +/// +/// [2.2.3.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/7b73e0c2-e0fc-46b1-9b03-50684ad2beba +#[derive(Debug, PartialEq, Clone)] +pub struct GetStatusChangeReturn { + pub return_code: ReturnCode, + pub reader_states: Vec, +} + +impl GetStatusChangeReturn { + const NAME: &'static str = "GetStatusChange_Return"; + + pub fn new(return_code: ReturnCode, reader_states: Vec) -> rpce::Pdu { + rpce::Pdu(Self { + return_code, + reader_states, + }) + } +} + +impl rpce::HeaderlessEncode for GetStatusChangeReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + let reader_states_len = cast_length!("GetStatusChangeReturn", "reader_states", self.reader_states.len())?; + let mut index = 0; + ndr::encode_ptr(Some(reader_states_len), &mut index, dst)?; + dst.write_u32(reader_states_len); + for reader_state in &self.reader_states { + reader_state.encode(dst)?; + } + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.return_code.size() // dst.write_u32(self.return_code.into()); + + ndr::ptr_size(true) // ndr::encode_ptr(Some(reader_states_len), &mut index, dst)?; + + 4 // dst.write_u32(reader_states_len); + + self.reader_states.iter().map(|_s| ReaderStateCommonCall::size()).sum::() + } +} + +/// [2.2.2.14] ConnectW_Call +/// +/// [2.2.2.14]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/fd06f6a0-a9ea-478c-9b5e-470fd9cde5a6 +#[derive(Debug, PartialEq, Clone)] +pub struct ConnectCall { + pub reader: String, + pub common: ConnectCommon, +} + +impl ConnectCall { + pub fn decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, charset)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for ConnectCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + let charset = expect_charset(charset)?; + let mut index = 0; + let _reader_ptr = ndr::decode_ptr(src, &mut index)?; + let mut common = ConnectCommon::decode_ptr(src, &mut index)?; + let reader = ndr::read_string_from_cursor(src, charset)?; + common.decode_value(src, None)?; + Ok(Self { reader, common }) + } +} + +/// [2.2.1.3] Connect_Common +/// +/// [2.2.1.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/32752f32-4410-4682-b9fc-9096674b52de +#[derive(Debug, PartialEq, Clone)] +pub struct ConnectCommon { + pub context: ScardContext, + pub share_mode: u32, + pub preferred_protocols: CardProtocol, +} + +impl ndr::Decode for ConnectCommon { + fn decode_ptr(src: &mut ReadCursor<'_>, index: &mut u32) -> DecodeResult + where + Self: Sized, + { + let context = ScardContext::decode_ptr(src, index)?; + ensure_size!(in: src, size: size_of::() * 2); + let share_mode = src.read_u32(); + let preferred_protocols = CardProtocol::from_bits_retain(src.read_u32()); + Ok(Self { + context, + share_mode, + preferred_protocols, + }) + } + + fn decode_value(&mut self, src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult<()> { + expect_no_charset(charset)?; + self.context.decode_value(src, None) + } +} + +bitflags! { + /// [2.2.5] Protocol Identifier + /// + /// [2.2.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/41673567-2710-4e86-be87-7b6f46fe10af + #[derive(Debug, PartialEq, Clone)] + pub struct CardProtocol: u32 { + const SCARD_PROTOCOL_UNDEFINED = 0x0000_0000; + const SCARD_PROTOCOL_T0 = 0x0000_0001; + const SCARD_PROTOCOL_T1 = 0x0000_0002; + const SCARD_PROTOCOL_TX = 0x0000_0003; + const SCARD_PROTOCOL_RAW = 0x0001_0000; + const SCARD_PROTOCOL_DEFAULT = 0x8000_0000; + const SCARD_PROTOCOL_OPTIMAL = 0x0000_0000; + } +} + +/// [2.2.1.2] REDIR_SCARDHANDLE +/// +/// [2.2.1.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/b6276356-7c5f-4d3e-be92-a6c85e58d008 +#[derive(Debug, PartialEq, Clone)] +pub struct ScardHandle { + pub context: ScardContext, + /// Shortcut: we always create 4-byte handle values. + /// The spec allows this field to have variable length. + pub value: u32, +} + +impl ScardHandle { + /// See [`ScardHandle::value`] + const VALUE_LENGTH: u32 = 4; + + pub fn new(context: ScardContext, value: u32) -> Self { + Self { context, value } + } +} + +impl ndr::Decode for ScardHandle { + fn decode_ptr(src: &mut ReadCursor<'_>, index: &mut u32) -> DecodeResult + where + Self: Sized, + { + let context = ScardContext::decode_ptr(src, index)?; + ensure_size!(ctx: "ScardHandle::decode_ptr", in: src, size: size_of::()); + let length = src.read_u32(); + if length != Self::VALUE_LENGTH { + error!(?length, "Unsupported value length in ScardHandle"); + return Err(invalid_field_err!( + "decode_ptr", + "unsupported value length in ScardHandle" + )); + } + let _ptr = ndr::decode_ptr(src, index)?; + Ok(Self { context, value: 0 }) + } + + fn decode_value(&mut self, src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult<()> { + expect_no_charset(charset)?; + self.context.decode_value(src, None)?; + ensure_size!(in: src, size: size_of::()); + let length = src.read_u32(); + if length != Self::VALUE_LENGTH { + error!(?length, "Unsupported value length in ScardHandle"); + return Err(invalid_field_err!( + "decode_value", + "unsupported value length in ScardHandle" + )); + } + ensure_size!(in: src, size: size_of::()); + self.value = src.read_u32(); + Ok(()) + } +} + +impl ndr::Encode for ScardHandle { + fn encode_ptr(&self, index: &mut u32, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + self.context.encode_ptr(index, dst)?; + ndr::encode_ptr(Some(Self::VALUE_LENGTH), index, dst)?; + Ok(()) + } + + fn encode_value(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size_value()); + self.context.encode_value(dst)?; + dst.write_u32(Self::VALUE_LENGTH); + dst.write_u32(self.value); + Ok(()) + } + + fn size_ptr(&self) -> usize { + self.context.size_ptr() + ndr::ptr_size(true) + } + + fn size_value(&self) -> usize { + self.context.size_value() + 4 /* cbHandle */ + 4 /* pbHandle */ + } +} + +/// [2.2.3.8] Connect_Return +/// +/// [2.2.3.8]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/ad9fbc8e-0963-44ac-8d71-38021685790c +#[derive(Debug, PartialEq, Clone)] +pub struct ConnectReturn { + pub return_code: ReturnCode, + pub handle: ScardHandle, + pub active_protocol: CardProtocol, +} + +impl ConnectReturn { + const NAME: &'static str = "Connect_Return"; + + pub fn new(return_code: ReturnCode, handle: ScardHandle, active_protocol: CardProtocol) -> rpce::Pdu { + rpce::Pdu(Self { + return_code, + handle, + active_protocol, + }) + } +} + +impl rpce::HeaderlessEncode for ConnectReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + let mut index = 0; + self.handle.encode_ptr(&mut index, dst)?; + dst.write_u32(self.active_protocol.bits()); + self.handle.encode_value(dst)?; + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.return_code.size() + self.handle.size() + 4 /* dwActiveProtocol */ + } +} + +/// [2.2.2.16] HCardAndDisposition_Call +/// +/// [2.2.2.16]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/f15ae865-9e99-4c5b-bb43-15a6b4885bd0 +#[derive(Debug, PartialEq, Clone)] +pub struct HCardAndDispositionCall { + pub handle: ScardHandle, + pub disposition: u32, +} + +impl HCardAndDispositionCall { + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, None)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for HCardAndDispositionCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + expect_no_charset(charset)?; + let mut index = 0; + let mut handle = ScardHandle::decode_ptr(src, &mut index)?; + ensure_size!(in: src, size: size_of::()); + let disposition = src.read_u32(); + handle.decode_value(src, None)?; + Ok(Self { handle, disposition }) + } +} + +/// [2.2.2.19] Transmit_Call +/// +/// [2.2.2.19]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/e3861cfa-e61b-4d64-b19d-f6b31e076beb +#[derive(Debug, PartialEq, Clone)] +pub struct TransmitCall { + pub handle: ScardHandle, + pub send_pci: SCardIORequest, + pub send_length: u32, + pub send_buffer: Vec, + pub recv_pci: Option, + pub recv_buffer_is_null: bool, + pub recv_length: u32, +} + +impl TransmitCall { + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, None)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for TransmitCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + expect_no_charset(charset)?; + let mut index = 0; + let mut handle = ScardHandle::decode_ptr(src, &mut index)?; + let mut send_pci = SCardIORequest::decode_ptr(src, &mut index)?; + ensure_size!(in: src, size: size_of::()); + let _send_length = src.read_u32(); + let _send_buffer_ptr = ndr::decode_ptr(src, &mut index)?; + let recv_pci_ptr = ndr::decode_ptr(src, &mut index)?; + ensure_size!(in: src, size: size_of::() * 2); + let recv_buffer_is_null = src.read_u32() == 1; + let recv_length = src.read_u32(); + + handle.decode_value(src, None)?; + send_pci.decode_value(src, None)?; + + ensure_size!(in: src, size: size_of::()); + let send_length = src.read_u32(); + let send_length_usize: usize = cast_length!("TransmitCall", "send_length", send_length)?; + ensure_size!(in: src, size: send_length_usize); + let send_buffer = src.read_slice(send_length_usize).to_vec(); + + let recv_pci = if recv_pci_ptr != 0 { + let mut recv_pci = SCardIORequest::decode_ptr(src, &mut index)?; + recv_pci.decode_value(src, None)?; + Some(recv_pci) + } else { + None + }; + + Ok(Self { + handle, + send_pci, + send_length, + send_buffer, + recv_pci, + recv_buffer_is_null, + recv_length, + }) + } +} + +/// [2.2.1.8] SCardIO_Request +/// +/// [2.2.1.8]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/f6e15da8-5bc0-4ef6-b28a-ce88e8415621 +#[derive(Debug, PartialEq, Clone)] +pub struct SCardIORequest { + pub protocol: CardProtocol, + pub extra_bytes_length: usize, + pub extra_bytes: Vec, +} + +impl ndr::Decode for SCardIORequest { + fn decode_ptr(src: &mut ReadCursor<'_>, index: &mut u32) -> DecodeResult + where + Self: Sized, + { + ensure_size!(in: src, size: size_of::() * 2); + let protocol = CardProtocol::from_bits_retain(src.read_u32()); + let extra_bytes_length = cast_length!("SCardIORequest", "extra_bytes_length", src.read_u32())?; + let _extra_bytes_ptr = ndr::decode_ptr(src, index)?; + let extra_bytes = Vec::new(); + Ok(Self { + protocol, + extra_bytes_length, + extra_bytes, + }) + } + + fn decode_value(&mut self, src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult<()> { + expect_no_charset(charset)?; + ensure_size!(in: src, size: self.extra_bytes_length); + self.extra_bytes = src.read_slice(self.extra_bytes_length).to_vec(); + Ok(()) + } +} + +impl ndr::Encode for SCardIORequest { + fn encode_ptr(&self, index: &mut u32, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size_ptr()); + + let extra_bytes_length = cast_length!("SCardIORequest", "extra_bytes_length", self.extra_bytes_length)?; + + dst.write_u32(self.protocol.bits()); + ndr::encode_ptr(Some(extra_bytes_length), index, dst) + } + + fn encode_value(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size_value()); + dst.write_slice(&self.extra_bytes); + Ok(()) + } + + fn size_ptr(&self) -> usize { + 4 /* dwProtocol */ + ndr::ptr_size(true) + } + + fn size_value(&self) -> usize { + self.extra_bytes_length + } +} + +/// [2.2.3.11] Transmit_Return +/// +/// [2.2.3.11]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/252cffd0-58b8-434d-9e1b-0d547544fb0f +#[derive(Debug, PartialEq, Clone)] +pub struct TransmitReturn { + pub return_code: ReturnCode, + pub recv_pci: Option, + pub recv_buffer: Vec, +} + +impl TransmitReturn { + const NAME: &'static str = "Transmit_Return"; + + pub fn new(return_code: ReturnCode, recv_pci: Option, recv_buffer: Vec) -> rpce::Pdu { + rpce::Pdu(Self { + return_code, + recv_pci, + recv_buffer, + }) + } +} + +impl rpce::HeaderlessEncode for TransmitReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + + let mut index = 0; + if let Some(recv_pci) = &self.recv_pci { + recv_pci.encode_ptr(&mut index, dst)?; + recv_pci.encode_value(dst)?; + } else { + dst.write_u32(0); // null value + } + + let recv_buffer_len: u32 = cast_length!("TransmitReturn", "recv_buffer_len", self.recv_buffer.len())?; + ndr::encode_ptr(Some(recv_buffer_len), &mut index, dst)?; + dst.write_u32(recv_buffer_len); + dst.write_slice(&self.recv_buffer); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.return_code.size() // dst.write_u32(self.return_code.into()); + + if let Some(recv_pci) = &self.recv_pci { + recv_pci.size() + } else { + 4 // null value + } + + ndr::ptr_size(true) // ndr::encode_ptr(Some(recv_buffer_len), &mut index, dst)?; + + 4 // dst.write_u32(recv_buffer_len); + + self.recv_buffer.len() // dst.write_slice(&self.recv_buffer); + } +} + +/// [2.2.2.18] Status_Call +/// +/// [2.2.2.18]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/f1139aed-e578-47f3-a800-f36b56c80500 +#[derive(Debug, PartialEq, Clone)] +pub struct StatusCall { + pub handle: ScardHandle, + pub reader_names_is_null: bool, + pub reader_length: u32, + pub atr_length: u32, +} + +impl StatusCall { + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, None)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for StatusCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + expect_no_charset(charset)?; + let mut index = 0; + let mut handle = ScardHandle::decode_ptr(src, &mut index)?; + ensure_size!(in: src, size: size_of::() * 3); + let reader_names_is_null = src.read_u32() == 1; + let reader_length = src.read_u32(); + let atr_length = src.read_u32(); + handle.decode_value(src, None)?; + Ok(Self { + handle, + reader_names_is_null, + reader_length, + atr_length, + }) + } +} + +/// [2.2.3.10] Status_Return +/// +/// [2.2.3.10]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/987c1358-ad6b-4c8e-88e1-06210c28a66f +#[derive(Debug, PartialEq, Clone)] +pub struct StatusReturn { + pub return_code: ReturnCode, + pub reader_names: Vec, + pub state: CardState, + pub protocol: CardProtocol, + pub atr: [u8; 32], + pub atr_length: u32, + + pub encoding: CharacterSet, +} + +impl StatusReturn { + const NAME: &'static str = "Status_Return"; + + pub fn new( + return_code: ReturnCode, + reader_names: Vec, + state: CardState, + protocol: CardProtocol, + atr: [u8; 32], + atr_length: u32, + encoding: CharacterSet, + ) -> rpce::Pdu { + rpce::Pdu(Self { + return_code, + reader_names, + state, + protocol, + atr, + atr_length, + encoding, + }) + } +} + +impl rpce::HeaderlessEncode for StatusReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + let mut index = 0; + let reader_names_length: u32 = cast_length!( + "StatusReturn", + "reader_names_length", + encoded_multistring_len(&self.reader_names, self.encoding) + )?; + ndr::encode_ptr(Some(reader_names_length), &mut index, dst)?; + dst.write_u32(self.state.into()); + dst.write_u32(self.protocol.bits()); + dst.write_slice(&self.atr); + dst.write_u32(self.atr_length); + dst.write_u32(reader_names_length); + write_multistring_to_cursor(dst, &self.reader_names, self.encoding)?; + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + size_of::() * 5 // dst.write_u32(self.return_code.into()); dst.write_u32(self.state.into()); dst.write_u32(self.protocol.bits()); dst.write_slice(&self.atr); dst.write_u32(self.atr_length); + + ndr::ptr_size(true) // ndr::encode_ptr(Some(reader_names_length), &mut index, dst)?; + + self.atr.len() // dst.write_slice(&self.atr); + + encoded_multistring_len(&self.reader_names, self.encoding) // write_multistring_to_cursor(dst, &self.reader_names, self.encoding)?; + } +} + +/// [2.2.4] Card/Reader State +/// +/// [2.2.4]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/264bc504-1195-43ff-a057-3d86a02c5d9c +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum CardState { + /// SCARD_UNKNOWN + Unknown = 0x0000_0000, + /// SCARD_ABSENT + Absent = 0x0000_0001, + /// SCARD_PRESENT + Present = 0x0000_0002, + /// SCARD_SWALLOWED + Swallowed = 0x0000_0003, + /// SCARD_POWERED + Powered = 0x0000_0004, + /// SCARD_NEGOTIABLE + Negotiable = 0x0000_0005, + /// SCARD_SPECIFICMODE + SpecificMode = 0x0000_0006, +} + +impl From for u32 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(val: CardState) -> Self { + val as u32 + } +} + +/// [2.2.2.2] Context_Call +/// +/// [2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/b11d26d9-c3d5-4e96-8d9f-aba35cded852 +#[derive(Debug, PartialEq, Clone)] +pub struct ContextCall { + pub context: ScardContext, +} + +impl ContextCall { + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, None)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for ContextCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + expect_no_charset(charset)?; + let mut index = 0; + let mut context = ScardContext::decode_ptr(src, &mut index)?; + context.decode_value(src, None)?; + Ok(Self { context }) + } +} + +/// [2.2.2.32] GetDeviceTypeId_Call +/// +/// [2.2.2.32]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/b5e18874-c42d-42ea-b1b1-3fd86a8a95f1 +#[derive(Debug, PartialEq, Clone)] +pub struct GetDeviceTypeIdCall { + pub context: ScardContext, + pub reader_ptr: u32, + pub reader_name: String, +} + +impl GetDeviceTypeIdCall { + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, None)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for GetDeviceTypeIdCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + expect_no_charset(charset)?; + let mut index = 0; + let mut context = ScardContext::decode_ptr(src, &mut index)?; + let reader_ptr = ndr::decode_ptr(src, &mut index)?; + context.decode_value(src, None)?; + let reader_name = ndr::read_string_from_cursor(src, CharacterSet::Unicode)?; + Ok(Self { + context, + reader_ptr, + reader_name, + }) + } +} + +/// [2.2.3.15] GetDeviceTypeId_Return +/// +/// [2.2.3.15]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/fed90d29-c41f-490a-86e9-7e88e42656b2 +#[derive(Debug, PartialEq, Clone)] +pub struct GetDeviceTypeIdReturn { + pub return_code: ReturnCode, + pub device_type_id: u32, +} + +impl GetDeviceTypeIdReturn { + const NAME: &'static str = "GetDeviceTypeId_Return"; + + pub fn new(return_code: ReturnCode, device_type_id: u32) -> rpce::Pdu { + rpce::Pdu(Self { + return_code, + device_type_id, + }) + } +} + +impl rpce::HeaderlessEncode for GetDeviceTypeIdReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + dst.write_u32(self.device_type_id); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.return_code.size() // dst.write_u32(self.return_code.into()); + + size_of::() // dst.write_u32(self.device_type_id); + } +} + +/// [2.2.2.26] ReadCacheW_Call +/// +/// [2.2.2.26]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/f45705cf-9299-4802-b408-685f02025e6a +#[derive(Debug, PartialEq, Clone)] +pub struct ReadCacheCall { + pub lookup_name: String, + pub common: ReadCacheCommon, +} + +impl ReadCacheCall { + pub fn decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, charset)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for ReadCacheCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + let charset = expect_charset(charset)?; + let mut index = 0; + let _lookup_name_ptr = ndr::decode_ptr(src, &mut index)?; + let mut common = ReadCacheCommon::decode_ptr(src, &mut index)?; + let lookup_name = ndr::read_string_from_cursor(src, charset)?; + common.decode_value(src, None)?; + Ok(Self { lookup_name, common }) + } +} + +/// [2.2.1.9] ReadCache_Common +/// +/// [2.2.1.9]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/3f9e07fa-66e2-498b-920c-39531709116b +#[derive(Debug, PartialEq, Clone)] +pub struct ReadCacheCommon { + pub context: ScardContext, + pub card_uuid: Vec, + pub freshness_counter: u32, + pub data_is_null: bool, + pub data_len: u32, +} + +impl ndr::Decode for ReadCacheCommon { + fn decode_ptr(src: &mut ReadCursor<'_>, index: &mut u32) -> DecodeResult + where + Self: Sized, + { + let context = ScardContext::decode_ptr(src, index)?; + let _card_uuid_ptr = ndr::decode_ptr(src, index)?; + ensure_size!(in: src, size: size_of::() * 2 + size_of::()); + let freshness_counter = src.read_u32(); + let data_is_null = src.read_i32() == 1; + let data_len = src.read_u32(); + + Ok(Self { + context, + card_uuid: Vec::new(), + freshness_counter, + data_is_null, + data_len, + }) + } + + fn decode_value(&mut self, src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult<()> { + expect_no_charset(charset)?; + self.context.decode_value(src, None)?; + ensure_size!(in: src, size: 16); + self.card_uuid = src.read_slice(16).to_vec(); + Ok(()) + } +} + +/// [2.2.3.1] ReadCache_Return +/// +/// [2.2.3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/da342355-e37f-485e-a490-3222a97fa356 +#[derive(Debug, PartialEq, Clone)] +pub struct ReadCacheReturn { + pub return_code: ReturnCode, + pub data: Vec, +} + +impl ReadCacheReturn { + const NAME: &'static str = "ReadCache_Return"; + + pub fn new(return_code: ReturnCode, data: Vec) -> rpce::Pdu { + rpce::Pdu(Self { return_code, data }) + } +} + +impl rpce::HeaderlessEncode for ReadCacheReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + let mut index = 0; + let data_len: u32 = cast_length!("ReadCacheReturn", "data_len", self.data.len())?; + ndr::encode_ptr(Some(data_len), &mut index, dst)?; + dst.write_u32(data_len); + dst.write_slice(&self.data); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.return_code.size() // dst.write_u32(self.return_code.into()); + + ndr::ptr_size(true) // ndr::encode_ptr(Some(data_len), &mut index, dst)?; + + size_of::() // dst.write_u32(data_len); + + self.data.len() // dst.write_slice(&self.data); + } +} + +/// [2.2.2.28] WriteCacheW_Call +/// +/// [2.2.2.28]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/3969bdcd-ecf3-42db-8bc6-2d6f970f9c67 +#[derive(Debug, PartialEq, Clone)] +pub struct WriteCacheCall { + pub lookup_name: String, + pub common: WriteCacheCommon, +} + +impl WriteCacheCall { + pub fn decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, charset)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for WriteCacheCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + let charset = expect_charset(charset)?; + let mut index = 0; + let _lookup_name_ptr = ndr::decode_ptr(src, &mut index)?; + let mut common = WriteCacheCommon::decode_ptr(src, &mut index)?; + let lookup_name = ndr::read_string_from_cursor(src, charset)?; + common.decode_value(src, None)?; + Ok(Self { lookup_name, common }) + } +} + +/// [2.2.1.10] WriteCache_Common +/// +/// [2.2.1.10]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/5604251b-9173-457c-9476-57863df9010e +#[derive(Debug, PartialEq, Clone)] +pub struct WriteCacheCommon { + pub context: ScardContext, + pub card_uuid: Vec, + pub freshness_counter: u32, + pub data: Vec, +} + +impl ndr::Decode for WriteCacheCommon { + fn decode_ptr(src: &mut ReadCursor<'_>, index: &mut u32) -> DecodeResult + where + Self: Sized, + { + let context = ScardContext::decode_ptr(src, index)?; + let _card_uuid_ptr = ndr::decode_ptr(src, index)?; + ensure_size!(in: src, size: size_of::() * 2); + let freshness_counter = src.read_u32(); + let _data_len = src.read_u32(); + let _data_ptr = ndr::decode_ptr(src, index)?; + + Ok(Self { + context, + card_uuid: Vec::new(), + freshness_counter, + data: Vec::new(), + }) + } + + fn decode_value(&mut self, src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult<()> { + expect_no_charset(charset)?; + self.context.decode_value(src, None)?; + ensure_size!(in: src, size: 16); + self.card_uuid = src.read_slice(16).to_vec(); + ensure_size!(in: src, size: size_of::()); + let data_len: usize = cast_length!("WriteCacheCommon", "data_len", src.read_u32())?; + ensure_size!(in: src, size: data_len); + self.data = src.read_slice(data_len).to_vec(); + Ok(()) + } +} + +/// [2.2.2.31] GetReaderIcon_Call +/// +/// [2.2.2.31]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/e6a68d90-697f-4b98-8ad6-f74853d27ccb +#[derive(Debug, PartialEq, Clone)] +pub struct GetReaderIconCall { + pub context: ScardContext, + pub reader_name: String, +} + +impl GetReaderIconCall { + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + Ok(rpce::Pdu::::decode(src, None)?.into_inner()) + } +} + +impl rpce::HeaderlessDecode for GetReaderIconCall { + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult { + expect_no_charset(charset)?; + let mut index = 0; + let mut context = ScardContext::decode_ptr(src, &mut index)?; + + let _reader_ptr = ndr::decode_ptr(src, &mut index)?; + + context.decode_value(src, None)?; + let reader_name = ndr::read_string_from_cursor(src, CharacterSet::Unicode)?; + Ok(Self { context, reader_name }) + } +} + +/// [2.2.3.14] GetReaderIcon_Return +/// +/// [2.2.3.14]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpesc/f011f3d9-e2a4-4c43-a336-4c89ecaa8360 +#[derive(Debug, PartialEq, Clone)] +pub struct GetReaderIconReturn { + pub return_code: ReturnCode, + pub data: Vec, +} + +impl GetReaderIconReturn { + const NAME: &'static str = "GetReaderIcon_Return"; + + pub fn new(return_code: ReturnCode, data: Vec) -> rpce::Pdu { + rpce::Pdu(Self { return_code, data }) + } +} + +impl rpce::HeaderlessEncode for GetReaderIconReturn { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + dst.write_u32(self.return_code.into()); + let data_len: u32 = cast_length!("GetReaderIconReturn", "data_len", self.data.len())?; + let mut index = 0; + ndr::encode_ptr(Some(data_len), &mut index, dst)?; + dst.write_u32(data_len); + dst.write_slice(&self.data); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + size_of::() // dst.write_u32(self.return_code.into()); + + ndr::ptr_size(true) // ndr::encode_ptr(Some(data_len), &mut index, dst)?; + + size_of::() // dst.write_u32(data_len); + + self.data.len() // dst.write_slice(&self.data); + } +} + +fn expect_charset(charset: Option) -> DecodeResult { + charset.ok_or_else(|| other_err!("internal error: missing character set")) +} + +fn expect_no_charset(charset: Option) -> DecodeResult<()> { + if charset.is_some() { + return Err(other_err!( + "internal error: character set given where none was expected" + )); + } + Ok(()) +} diff --git a/crates/ironrdp-rdpdr/src/pdu/esc/ndr.rs b/crates/ironrdp-rdpdr/src/pdu/esc/ndr.rs new file mode 100644 index 00000000..8bc4e3e2 --- /dev/null +++ b/crates/ironrdp-rdpdr/src/pdu/esc/ndr.rs @@ -0,0 +1,101 @@ +//! Request/response messages are nested structs with fields, encoded as NDR (network data +//! representation). +//! +//! Fixed-size fields are encoded in-line as they appear in the struct. +//! +//! Variable-sized fields (strings, byte arrays, sometimes structs) are encoded as pointers: +//! - in place of the field in the struct, a "pointer" is written +//! - the pointer value is 0x0002xxxx, where xxxx is an "index" in increments of 4 +//! - for example, first pointer is 0x0002_0000, second is 0x0002_0004, third is 0x0002_0008 etc. +//! - the actual values are then appended at the end of the message, in the same order as their +//! pointers appeared +//! - in the code below, "*_ptr" is the pointer value and "*_value" the actual data +//! - note that some fields (like arrays) will have a length prefix before the pointer and also +//! before the actual data at the end of the message +//! +//! To deal with this, fixed-size structs only have encode/decode methods, while variable-size ones +//! have encode_ptr/decode_ptr and encode_value/decode_value methods. Messages are parsed linearly, +//! so decode_ptr/decode_value are called at different stages (same for encoding). +//! +//! Most of the above was reverse-engineered from FreeRDP: [smartcard_pack.c] +//! +//! [smartcard_pack.c]: https://github.com/FreeRDP/FreeRDP/blob/ff303a9bda911c54ffc1b9f2471acd79c897b075/libfreerdp/utils/smartcard_pack.c + +use ironrdp_core::{ensure_size, invalid_field_err, DecodeResult, EncodeResult, ReadCursor, WriteCursor}; +use ironrdp_pdu::utils::{self, CharacterSet}; + +pub trait Decode { + fn decode_ptr(src: &mut ReadCursor<'_>, index: &mut u32) -> DecodeResult + where + Self: Sized; + fn decode_value(&mut self, src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult<()>; +} + +pub trait Encode { + fn encode_ptr(&self, index: &mut u32, dst: &mut WriteCursor<'_>) -> EncodeResult<()>; + fn encode_value(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()>; + fn size_ptr(&self) -> usize; + fn size_value(&self) -> usize; + fn size(&self) -> usize { + self.size_ptr() + self.size_value() + } +} + +pub fn encode_ptr(length: Option, index: &mut u32, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(ctx: "encode_ptr", in: dst, size: ptr_size(length.is_some())); + if let Some(length) = length { + dst.write_u32(length); + } + + dst.write_u32(0x0002_0000 + *index * 4); + *index += 1; + Ok(()) +} + +pub fn decode_ptr(src: &mut ReadCursor<'_>, index: &mut u32) -> DecodeResult { + ensure_size!(ctx: "decode_ptr", in: src, size: size_of::()); + let ptr = src.read_u32(); + if ptr == 0 { + // NULL pointer is OK. Don't update index. + return Ok(ptr); + } + let expect_ptr = 0x0002_0000 + *index * 4; + *index += 1; + if ptr != expect_ptr { + Err(invalid_field_err!("decode_ptr", "ptr", "ptr != expect_ptr")) + } else { + Ok(ptr) + } +} + +pub fn ptr_size(with_length: bool) -> usize { + if with_length { + size_of::() * 2 + } else { + size_of::() + } +} + +/// A special read_string_from_cursor which reads and ignores the additional length and +/// offset fields prefixing the string, as well as any extra padding for a 4-byte aligned +/// NULL-terminated string. +pub fn read_string_from_cursor(cursor: &mut ReadCursor<'_>, charset: CharacterSet) -> DecodeResult { + const ALIGNMENT: usize = 4; + ensure_size!(ctx: "ndr::read_string_from_cursor", in: cursor, size: size_of::() * 3); + let _length = cursor.read_u32(); + let _offset = cursor.read_u32(); + let _length2 = cursor.read_u32(); + + let string = utils::read_string_from_cursor(cursor, charset, true)?; + + // Skip padding for 4-byte aligned NULL-terminated string. + let mut pad = cursor.pos(); + let size = (pad + ALIGNMENT - 1) & !(ALIGNMENT - 1); + pad = size - pad; + if pad > 0 { + ensure_size!(ctx: "ndr::read_string_from_cursor", in: cursor, size: pad); + cursor.advance(pad); + } + + Ok(string) +} diff --git a/crates/ironrdp-rdpdr/src/pdu/esc/rpce.rs b/crates/ironrdp-rdpdr/src/pdu/esc/rpce.rs new file mode 100644 index 00000000..75f6a90f --- /dev/null +++ b/crates/ironrdp-rdpdr/src/pdu/esc/rpce.rs @@ -0,0 +1,305 @@ +//! PDUs for [\[MS-RPCE\]: Remote Procedure Call Protocol Extensions] as required by [MS-RDPESC]. +//! +//! [\[MS-RPCE\]: Remote Procedure Call Protocol Extensions]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/290c38b1-92fe-4229-91e6-4fc376610c15 + +use ironrdp_core::{ + cast_length, ensure_size, invalid_field_err, DecodeError, DecodeResult, EncodeResult, ReadCursor, WriteCursor, +}; +use ironrdp_pdu::utils::CharacterSet; + +/// Wrapper struct for [MS-RPCE] PDUs that allows for common [`Encode`], [`Encode`], and [`Self::decode`] implementations. +/// +/// Structs which are meant to be encoded into an [MS-RPCE] message should typically implement [`HeaderlessEncode`], +/// and their `new` function should return a [`Pdu`] wrapping the underlying struct. +/// +/// ```rust +/// #[derive(Debug)] +/// pub struct RpceEncodePdu { +/// example_field: u32, +/// } +/// +/// impl RpceEncodePdu { +/// /// `new` returns a `Pdu` wrapping the underlying struct. +/// pub fn new(example_field: u32) -> rpce::Pdu { +/// rpce::Pdu(Self { example_field }) +/// } +/// } +/// +/// /// The underlying struct should implement `HeaderlessEncode`. +/// impl rpce::HeaderlessEncode for RpceEncodePdu { +/// fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { +/// ensure_size!(in: dst, size: self.size()); +/// dst.write_u32(self.return_code.into()); +/// Ok(()) +/// } +/// +/// fn name(&self) -> &'static str { +/// "RpceEncodePdu" +/// } +/// +/// fn size(&self) -> usize { +/// std::mem::size_of() +/// } +/// } +/// ``` +/// +/// See [`super::LongReturn`] for a live example of an encodable PDU. +/// +/// Structs which are meant to be decoded from an [MS-RPCE] message should typically implement [`HeaderlessDecode`], +/// and their `decode` function should return a [`Pdu`] wrapping the underlying struct. +/// +/// ```rust +/// pub struct RpceDecodePdu { +/// example_field: u32, +/// } +/// +/// impl RpceDecodePdu { +/// /// `decode` returns a `Pdu` wrapping the underlying struct. +/// pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult> { +/// Ok(rpce::Pdu::::decode(src)?.into_inner()) +/// } +/// +/// fn size() -> usize { +/// std::mem::size_of() +/// } +/// } +/// +/// /// The underlying struct should implement `HeaderlessDecode`. +/// impl rpce::HeaderlessDecode for RpceDecodePdu { +/// fn decode(src: &mut ReadCursor<'_>) -> DecodeResult +/// where +/// Self: Sized, +/// { +/// ensure_size!(in: src, size: Self::size()); +/// let example_field = src.read_u32(); +/// Ok(Self { example_field }) +/// } +/// } +/// ``` +/// +/// See [`super::EstablishContextCall`] for a live example of a decodable PDU. +#[derive(Debug)] +pub struct Pdu(pub T); + +impl Pdu { + pub fn into_inner(self) -> T { + self.0 + } + + pub fn into_inner_ref(&self) -> &T { + &self.0 + } +} + +impl Pdu { + /// Decodes the instance from a buffer stripping it of its headers. + pub fn decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult> { + // We expect `StreamHeader::decode`, `TypeHeader::decode`, and `T::decode` to each + // call `ensure_size!` to ensure that the buffer is large enough, so we can safely + // omit that check here. + let _stream_header = StreamHeader::decode(src)?; + let _type_header = TypeHeader::decode(src)?; + let pdu = T::headerless_decode(src, charset)?; + Ok(Self(pdu)) + } +} + +impl ironrdp_core::Encode for Pdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(ctx: self.name(), in: dst, size: self.size()); + let stream_header = StreamHeader::default(); + let type_header = TypeHeader::new(cast_length!("Pdu", "size", self.size())?); + + stream_header.encode(dst)?; + type_header.encode(dst)?; + HeaderlessEncode::encode(&self.0, dst)?; + + // Pad response to be 8-byte aligned. + let padding_size = padding_size(&self.0); + if padding_size > 0 { + dst.write_slice(&vec![0; padding_size]); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + self.0.name() + } + + fn size(&self) -> usize { + StreamHeader::size() + TypeHeader::size() + HeaderlessEncode::size(&self.0) + padding_size(&self.0) + } +} + +impl Encode for Pdu {} + +/// Trait for types that can be encoded into an [MS-RPCE] message. +/// +/// Implementers should typically avoid implementing this trait directly +/// and instead implement [`HeaderlessEncode`], and wrap it in a [`Pdu`]. +pub trait Encode: ironrdp_core::Encode + Send + core::fmt::Debug {} + +/// Trait for types that can be encoded into an [MS-RPCE] message. +/// +/// Implementers should typically implement this trait instead of [`Encode`]. +pub trait HeaderlessEncode: Send + core::fmt::Debug { + /// Encodes the instance into a buffer sans its headers. + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()>; + /// Returns the name associated with this RPCE PDU. + fn name(&self) -> &'static str; + /// Returns the size of the instance sans its headers. + fn size(&self) -> usize; +} + +/// Trait for types that can be decoded from an [MS-RPCE] message. +/// +/// Implementers should typically implement this trait for a given type `T` +/// and then call [`Pdu::decode`] to decode the instance. See [`Pdu`] for more +/// details and an example. +pub trait HeaderlessDecode: Sized { + /// Decodes the instance from a buffer sans its headers. + /// + /// `charset` is an optional parameter that can be used to specify the character set + /// when relevant. This is useful for accounting for the "A" vs "W" variants of certain + /// opcodes e.g. [`ListReadersA`][`super::ScardIoCtlCode::ListReadersA`] vs [`ListReadersW`][`super::ScardIoCtlCode::ListReadersW`]. + fn headerless_decode(src: &mut ReadCursor<'_>, charset: Option) -> DecodeResult; +} + +/// [2.2.6.1] Common Type Header for the Serialization Stream +/// +/// [2.2.6.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/6d75d40e-e2d2-4420-b9e9-8508a726a9ae +struct StreamHeader { + version: u8, + endianness: Endianness, + common_header_length: u16, + filler: u32, +} + +impl Default for StreamHeader { + fn default() -> Self { + Self { + version: 1, + endianness: Endianness::LittleEndian, + common_header_length: 8, + filler: 0xCCCC_CCCC, + } + } +} + +impl StreamHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + dst.write_u8(self.version); + dst.write_u8(self.endianness.into()); + dst.write_u16(self.common_header_length); + dst.write_u32(self.filler); + Ok(()) + } + + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::size()); + let version = src.read_u8(); + let endianness = Endianness::try_from(src.read_u8())?; + let common_header_length = src.read_u16(); + let filler = src.read_u32(); + if endianness == Endianness::LittleEndian { + Ok(Self { + version, + endianness, + common_header_length, + filler, + }) + } else { + Err(invalid_field_err!( + "decode", + "StreamHeader", + "server returned big-endian data, parsing not implemented" + )) + } + } + + fn size() -> usize { + size_of::() + size_of::() + size_of::() + size_of::() + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +enum Endianness { + BigEndian = 0x00, + LittleEndian = 0x10, +} + +impl TryFrom for Endianness { + type Error = DecodeError; + + fn try_from(value: u8) -> Result { + match value { + 0x00 => Ok(Endianness::BigEndian), + 0x10 => Ok(Endianness::LittleEndian), + _ => Err(invalid_field_err!("try_from", "RpceEndianness", "unsupported value")), + } + } +} + +impl From for u8 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(endianness: Endianness) -> Self { + endianness as u8 + } +} + +/// [2.2.6.2] Private Header for Constructed Type +/// +/// [2.2.6.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/63949ba8-bc88-4c0c-9377-23f14b197827 +#[derive(Debug)] +struct TypeHeader { + object_buffer_length: u32, + filler: u32, +} + +impl TypeHeader { + fn new(object_buffer_length: u32) -> Self { + Self { + object_buffer_length, + filler: 0, + } + } + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::size()); + dst.write_u32(self.object_buffer_length); + dst.write_u32(self.filler); + Ok(()) + } + + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::size()); + let object_buffer_length = src.read_u32(); + let filler = src.read_u32(); + + Ok(Self { + object_buffer_length, + filler, + }) + } + + fn size() -> usize { + size_of::() * 2 + } +} + +/// Calculates the padding required for an [MS-RPCE] message +/// to be 8-byte aligned. +fn padding_size(pdu: &impl HeaderlessEncode) -> usize { + let tail = pdu.size() % 8; + if tail > 0 { + 8 - tail + } else { + 0 + } +} diff --git a/crates/ironrdp-rdpdr/src/pdu/mod.rs b/crates/ironrdp-rdpdr/src/pdu/mod.rs new file mode 100644 index 00000000..33848882 --- /dev/null +++ b/crates/ironrdp-rdpdr/src/pdu/mod.rs @@ -0,0 +1,468 @@ +use core::fmt::{self, Display}; + +use ironrdp_core::{ + ensure_size, invalid_field_err, unsupported_value_err, Decode, DecodeError, DecodeResult, Encode, EncodeResult, + ReadCursor, WriteCursor, +}; +use ironrdp_svc::SvcEncode; + +use self::efs::{ + ClientDeviceListAnnounce, ClientDeviceListRemove, ClientDriveQueryDirectoryResponse, + ClientDriveQueryInformationResponse, ClientDriveQueryVolumeInformationResponse, ClientDriveSetInformationResponse, + ClientNameRequest, CoreCapability, CoreCapabilityKind, DeviceCloseResponse, DeviceControlResponse, + DeviceCreateResponse, DeviceIoRequest, DeviceReadResponse, DeviceWriteResponse, ServerDeviceAnnounceResponse, + VersionAndIdPdu, VersionAndIdPduKind, +}; + +pub mod efs; +pub mod esc; + +/// All available RDPDR PDUs. +pub enum RdpdrPdu { + VersionAndIdPdu(VersionAndIdPdu), + ClientNameRequest(ClientNameRequest), + CoreCapability(CoreCapability), + ClientDeviceListAnnounce(ClientDeviceListAnnounce), + ClientDeviceListRemove(ClientDeviceListRemove), + ServerDeviceAnnounceResponse(ServerDeviceAnnounceResponse), + DeviceIoRequest(DeviceIoRequest), + DeviceControlResponse(DeviceControlResponse), + DeviceCreateResponse(DeviceCreateResponse), + ClientDriveQueryInformationResponse(ClientDriveQueryInformationResponse), + DeviceCloseResponse(DeviceCloseResponse), + ClientDriveQueryDirectoryResponse(ClientDriveQueryDirectoryResponse), + ClientDriveQueryVolumeInformationResponse(ClientDriveQueryVolumeInformationResponse), + DeviceReadResponse(DeviceReadResponse), + DeviceWriteResponse(DeviceWriteResponse), + ClientDriveSetInformationResponse(ClientDriveSetInformationResponse), + UserLoggedon, + EmptyResponse, +} + +impl RdpdrPdu { + /// Returns the [`SharedHeader`] of the PDU. + fn header(&self) -> SharedHeader { + match self { + RdpdrPdu::VersionAndIdPdu(pdu) => match pdu.kind { + VersionAndIdPduKind::ClientAnnounceReply => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreClientidConfirm, + }, + VersionAndIdPduKind::ServerAnnounceRequest => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreServerAnnounce, + }, + VersionAndIdPduKind::ServerClientIdConfirm => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreClientidConfirm, + }, + }, + RdpdrPdu::ClientNameRequest(_) => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreClientName, + }, + RdpdrPdu::CoreCapability(pdu) => match pdu.kind { + CoreCapabilityKind::ServerCoreCapabilityRequest => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreServerCapability, + }, + CoreCapabilityKind::ClientCoreCapabilityResponse => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreClientCapability, + }, + }, + RdpdrPdu::ClientDeviceListAnnounce(_) => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreDevicelistAnnounce, + }, + RdpdrPdu::ClientDeviceListRemove(_) => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreDevicelistRemove, + }, + RdpdrPdu::ServerDeviceAnnounceResponse(_) => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreDeviceReply, + }, + RdpdrPdu::DeviceIoRequest(_) => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreDeviceIoRequest, + }, + RdpdrPdu::DeviceControlResponse(_) + | RdpdrPdu::DeviceCreateResponse(_) + | RdpdrPdu::ClientDriveQueryInformationResponse(_) + | RdpdrPdu::DeviceCloseResponse(_) + | RdpdrPdu::ClientDriveQueryDirectoryResponse(_) + | RdpdrPdu::ClientDriveQueryVolumeInformationResponse(_) + | RdpdrPdu::DeviceReadResponse(_) + | RdpdrPdu::DeviceWriteResponse(_) + | RdpdrPdu::ClientDriveSetInformationResponse(_) + | RdpdrPdu::EmptyResponse => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreDeviceIoCompletion, + }, + RdpdrPdu::UserLoggedon => SharedHeader { + component: Component::RdpdrCtypCore, + packet_id: PacketId::CoreUserLoggedon, + }, + } + } +} + +impl Decode<'_> for RdpdrPdu { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + let header = SharedHeader::decode(src)?; + match header.packet_id { + PacketId::CoreServerAnnounce => Ok(RdpdrPdu::VersionAndIdPdu(VersionAndIdPdu::decode(header, src)?)), + PacketId::CoreServerCapability => Ok(RdpdrPdu::CoreCapability(CoreCapability::decode(header, src)?)), + PacketId::CoreClientidConfirm => Ok(RdpdrPdu::VersionAndIdPdu(VersionAndIdPdu::decode(header, src)?)), + PacketId::CoreDeviceReply => Ok(RdpdrPdu::ServerDeviceAnnounceResponse( + ServerDeviceAnnounceResponse::decode(src)?, + )), + PacketId::CoreDeviceIoRequest => Ok(RdpdrPdu::DeviceIoRequest(DeviceIoRequest::decode(src)?)), + PacketId::CoreUserLoggedon => Ok(RdpdrPdu::UserLoggedon), + _ => Err(unsupported_value_err!( + "RdpdrPdu", + "PacketId", + header.packet_id.to_string() + )), + } + } +} + +impl Encode for RdpdrPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + self.header().encode(dst)?; + + match self { + RdpdrPdu::VersionAndIdPdu(pdu) => pdu.encode(dst), + RdpdrPdu::ClientNameRequest(pdu) => pdu.encode(dst), + RdpdrPdu::CoreCapability(pdu) => pdu.encode(dst), + RdpdrPdu::ClientDeviceListAnnounce(pdu) => pdu.encode(dst), + RdpdrPdu::ClientDeviceListRemove(pdu) => pdu.encode(dst), + RdpdrPdu::ServerDeviceAnnounceResponse(pdu) => pdu.encode(dst), + RdpdrPdu::DeviceIoRequest(pdu) => pdu.encode(dst), + RdpdrPdu::DeviceControlResponse(pdu) => pdu.encode(dst), + RdpdrPdu::DeviceCreateResponse(pdu) => pdu.encode(dst), + RdpdrPdu::ClientDriveQueryInformationResponse(pdu) => pdu.encode(dst), + RdpdrPdu::DeviceCloseResponse(pdu) => pdu.encode(dst), + RdpdrPdu::ClientDriveQueryDirectoryResponse(pdu) => pdu.encode(dst), + RdpdrPdu::ClientDriveQueryVolumeInformationResponse(pdu) => pdu.encode(dst), + RdpdrPdu::DeviceReadResponse(pdu) => pdu.encode(dst), + RdpdrPdu::DeviceWriteResponse(pdu) => pdu.encode(dst), + RdpdrPdu::ClientDriveSetInformationResponse(pdu) => pdu.encode(dst), + RdpdrPdu::UserLoggedon => Ok(()), + RdpdrPdu::EmptyResponse => { + // https://github.com/FreeRDP/FreeRDP/blob/dfa231c0a55b005af775b833f92f6bcd30363d77/channels/drive/client/drive_main.c#L601 + dst.write_u32(0); + Ok(()) + } + } + } + + fn name(&self) -> &'static str { + match self { + RdpdrPdu::VersionAndIdPdu(pdu) => pdu.name(), + RdpdrPdu::ClientNameRequest(pdu) => pdu.name(), + RdpdrPdu::CoreCapability(pdu) => pdu.name(), + RdpdrPdu::ClientDeviceListAnnounce(pdu) => pdu.name(), + RdpdrPdu::ClientDeviceListRemove(pdu) => pdu.name(), + RdpdrPdu::ServerDeviceAnnounceResponse(pdu) => pdu.name(), + RdpdrPdu::DeviceIoRequest(pdu) => pdu.name(), + RdpdrPdu::DeviceControlResponse(pdu) => pdu.name(), + RdpdrPdu::DeviceCreateResponse(pdu) => pdu.name(), + RdpdrPdu::ClientDriveQueryInformationResponse(pdu) => pdu.name(), + RdpdrPdu::DeviceCloseResponse(pdu) => pdu.name(), + RdpdrPdu::ClientDriveQueryDirectoryResponse(pdu) => pdu.name(), + RdpdrPdu::ClientDriveQueryVolumeInformationResponse(pdu) => pdu.name(), + RdpdrPdu::DeviceReadResponse(pdu) => pdu.name(), + RdpdrPdu::DeviceWriteResponse(pdu) => pdu.name(), + RdpdrPdu::ClientDriveSetInformationResponse(pdu) => pdu.name(), + RdpdrPdu::UserLoggedon => "UserLoggedon", + RdpdrPdu::EmptyResponse => "EmptyResponse", + } + } + + fn size(&self) -> usize { + SharedHeader::SIZE + + match self { + RdpdrPdu::VersionAndIdPdu(pdu) => pdu.size(), + RdpdrPdu::ClientNameRequest(pdu) => pdu.size(), + RdpdrPdu::CoreCapability(pdu) => pdu.size(), + RdpdrPdu::ClientDeviceListAnnounce(pdu) => pdu.size(), + RdpdrPdu::ClientDeviceListRemove(pdu) => pdu.size(), + RdpdrPdu::ServerDeviceAnnounceResponse(pdu) => pdu.size(), + RdpdrPdu::DeviceIoRequest(pdu) => pdu.size(), + RdpdrPdu::DeviceControlResponse(pdu) => pdu.size(), + RdpdrPdu::DeviceCreateResponse(pdu) => pdu.size(), + RdpdrPdu::ClientDriveQueryInformationResponse(pdu) => pdu.size(), + RdpdrPdu::DeviceCloseResponse(pdu) => pdu.size(), + RdpdrPdu::ClientDriveQueryDirectoryResponse(pdu) => pdu.size(), + RdpdrPdu::ClientDriveQueryVolumeInformationResponse(pdu) => pdu.size(), + RdpdrPdu::DeviceReadResponse(pdu) => pdu.size(), + RdpdrPdu::DeviceWriteResponse(pdu) => pdu.size(), + RdpdrPdu::ClientDriveSetInformationResponse(pdu) => pdu.size(), + RdpdrPdu::UserLoggedon => 0, + RdpdrPdu::EmptyResponse => size_of::(), + } + } +} + +impl SvcEncode for RdpdrPdu {} + +impl fmt::Debug for RdpdrPdu { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::VersionAndIdPdu(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::ClientNameRequest(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::CoreCapability(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::ClientDeviceListAnnounce(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::ClientDeviceListRemove(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::ServerDeviceAnnounceResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::DeviceIoRequest(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::DeviceControlResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::DeviceCreateResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::ClientDriveQueryInformationResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::DeviceCloseResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::ClientDriveQueryDirectoryResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::ClientDriveQueryVolumeInformationResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::DeviceReadResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::DeviceWriteResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::ClientDriveSetInformationResponse(it) => { + write!(f, "RdpdrPdu({it:?})") + } + Self::UserLoggedon => { + write!(f, "RdpdrPdu(UserLoggedon)") + } + Self::EmptyResponse => { + write!(f, "RdpdrPdu(EmptyResponse)") + } + } + } +} + +impl From for RdpdrPdu { + fn from(value: DeviceControlResponse) -> Self { + Self::DeviceControlResponse(value) + } +} + +impl From for RdpdrPdu { + fn from(value: DeviceCreateResponse) -> Self { + Self::DeviceCreateResponse(value) + } +} + +impl From for RdpdrPdu { + fn from(value: ClientDriveQueryInformationResponse) -> Self { + Self::ClientDriveQueryInformationResponse(value) + } +} + +impl From for RdpdrPdu { + fn from(value: DeviceCloseResponse) -> Self { + Self::DeviceCloseResponse(value) + } +} + +impl From for RdpdrPdu { + fn from(value: ClientDriveQueryDirectoryResponse) -> Self { + Self::ClientDriveQueryDirectoryResponse(value) + } +} + +impl From for RdpdrPdu { + fn from(value: ClientDriveQueryVolumeInformationResponse) -> Self { + Self::ClientDriveQueryVolumeInformationResponse(value) + } +} + +impl From for RdpdrPdu { + fn from(value: DeviceReadResponse) -> Self { + Self::DeviceReadResponse(value) + } +} + +impl From for RdpdrPdu { + fn from(value: DeviceWriteResponse) -> Self { + Self::DeviceWriteResponse(value) + } +} + +impl From for RdpdrPdu { + fn from(value: ClientDriveSetInformationResponse) -> Self { + Self::ClientDriveSetInformationResponse(value) + } +} + +/// [2.2.1.1] Shared Header (RDPDR_HEADER), a header that is shared by all RDPDR PDUs. +/// +/// [2.2.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/29d4108f-8163-4a67-8271-e48c4b9c2a7c +#[derive(Debug)] +pub struct SharedHeader { + pub component: Component, + pub packet_id: PacketId, +} + +impl SharedHeader { + const SIZE: usize = size_of::() * 2; + + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: Self::SIZE); + dst.write_u16(self.component.into()); + dst.write_u16(self.packet_id.into()); + Ok(()) + } + + pub fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: Self::SIZE); + Ok(Self { + component: src.read_u16().try_into()?, + packet_id: src.read_u16().try_into()?, + }) + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(u16)] +pub enum Component { + /// RDPDR_CTYP_CORE + RdpdrCtypCore = 0x4472, + /// RDPDR_CTYP_PRN + RdpdrCtypPrn = 0x5052, +} + +impl TryFrom for Component { + type Error = DecodeError; + + fn try_from(value: u16) -> Result { + match value { + 0x4472 => Ok(Component::RdpdrCtypCore), + 0x5052 => Ok(Component::RdpdrCtypPrn), + _ => Err(invalid_field_err!("try_from", "Component", "invalid value")), + } + } +} + +impl From for u16 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(component: Component) -> Self { + component as u16 + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(u16)] +pub enum PacketId { + /// PAKID_CORE_SERVER_ANNOUNCE + CoreServerAnnounce = 0x496E, + /// PAKID_CORE_CLIENTID_CONFIRM + CoreClientidConfirm = 0x4343, + /// PAKID_CORE_CLIENT_NAME + CoreClientName = 0x434E, + /// PAKID_CORE_DEVICELIST_ANNOUNCE + CoreDevicelistAnnounce = 0x4441, + /// PAKID_CORE_DEVICE_REPLY + CoreDeviceReply = 0x6472, + /// PAKID_CORE_DEVICE_IOREQUEST + CoreDeviceIoRequest = 0x4952, + /// PAKID_CORE_DEVICE_IOCOMPLETION + CoreDeviceIoCompletion = 0x4943, + /// PAKID_CORE_SERVER_CAPABILITY + CoreServerCapability = 0x5350, + /// PAKID_CORE_CLIENT_CAPABILITY + CoreClientCapability = 0x4350, + /// PAKID_CORE_DEVICELIST_REMOVE + CoreDevicelistRemove = 0x444D, + /// PAKID_PRN_CACHE_DATA + PrnCacheData = 0x5043, + /// PAKID_CORE_USER_LOGGEDON + CoreUserLoggedon = 0x554C, + /// PAKID_PRN_USING_XPS + PrnUsingXps = 0x5543, +} + +impl TryFrom for PacketId { + type Error = DecodeError; + + fn try_from(value: u16) -> Result { + match value { + 0x496E => Ok(PacketId::CoreServerAnnounce), + 0x4343 => Ok(PacketId::CoreClientidConfirm), + 0x434E => Ok(PacketId::CoreClientName), + 0x4441 => Ok(PacketId::CoreDevicelistAnnounce), + 0x6472 => Ok(PacketId::CoreDeviceReply), + 0x4952 => Ok(PacketId::CoreDeviceIoRequest), + 0x4943 => Ok(PacketId::CoreDeviceIoCompletion), + 0x5350 => Ok(PacketId::CoreServerCapability), + 0x4350 => Ok(PacketId::CoreClientCapability), + 0x444D => Ok(PacketId::CoreDevicelistRemove), + 0x5043 => Ok(PacketId::PrnCacheData), + 0x554C => Ok(PacketId::CoreUserLoggedon), + 0x5543 => Ok(PacketId::PrnUsingXps), + _ => Err(invalid_field_err!("try_from", "PacketId", "invalid value")), + } + } +} + +impl Display for PacketId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PacketId::CoreServerAnnounce => write!(f, "PAKID_CORE_SERVER_ANNOUNCE"), + PacketId::CoreClientidConfirm => write!(f, "PAKID_CORE_CLIENTID_CONFIRM"), + PacketId::CoreClientName => write!(f, "PAKID_CORE_CLIENT_NAME"), + PacketId::CoreDevicelistAnnounce => write!(f, "PAKID_CORE_DEVICELIST_ANNOUNCE"), + PacketId::CoreDeviceReply => write!(f, "PAKID_CORE_DEVICE_REPLY"), + PacketId::CoreDeviceIoRequest => write!(f, "PAKID_CORE_DEVICE_IOREQUEST"), + PacketId::CoreDeviceIoCompletion => write!(f, "PAKID_CORE_DEVICE_IOCOMPLETION"), + PacketId::CoreServerCapability => write!(f, "PAKID_CORE_SERVER_CAPABILITY"), + PacketId::CoreClientCapability => write!(f, "PAKID_CORE_CLIENT_CAPABILITY"), + PacketId::CoreDevicelistRemove => write!(f, "PAKID_CORE_DEVICELIST_REMOVE"), + PacketId::PrnCacheData => write!(f, "PAKID_PRN_CACHE_DATA"), + PacketId::CoreUserLoggedon => write!(f, "PAKID_CORE_USER_LOGGEDON"), + PacketId::PrnUsingXps => write!(f, "PAKID_PRN_USING_XPS"), + } + } +} + +impl From for u16 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(packet_id: PacketId) -> Self { + packet_id as u16 + } +} diff --git a/crates/ironrdp-rdpfile/Cargo.toml b/crates/ironrdp-rdpfile/Cargo.toml new file mode 100644 index 00000000..f250a9df --- /dev/null +++ b/crates/ironrdp-rdpfile/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ironrdp-rdpfile" +version = "0.1.0" +readme = "README.md" +description = "Parser and writer for .RDP file format" +publish = false # TODO: publish +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } # public + +[lints] +workspace = true diff --git a/crates/ironrdp-rdpfile/README.md b/crates/ironrdp-rdpfile/README.md new file mode 100644 index 00000000..3d9d8483 --- /dev/null +++ b/crates/ironrdp-rdpfile/README.md @@ -0,0 +1,7 @@ +# IronRDP .RDP file + +Loader and writer for the .RDP file format. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-rdpfile/src/lib.rs b/crates/ironrdp-rdpfile/src/lib.rs new file mode 100644 index 00000000..37519259 --- /dev/null +++ b/crates/ironrdp-rdpfile/src/lib.rs @@ -0,0 +1,126 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![no_std] + +extern crate alloc; + +use alloc::borrow::ToOwned as _; +use alloc::string::{String, ToString as _}; +use alloc::vec::Vec; +use core::fmt; + +use ironrdp_propertyset::{PropertySet, Value}; + +#[derive(Debug, Clone)] +pub enum ErrorKind { + UnknownType { ty: String }, + InvalidValue { ty: String, value: String }, + MalformedLine { line: String }, +} + +#[derive(Debug, Clone)] +pub struct Error { + pub kind: ErrorKind, + pub line: usize, +} + +impl core::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let line_number = self.line; + + match &self.kind { + ErrorKind::UnknownType { ty } => write!(f, "unknown type at line {line_number} ({ty})"), + ErrorKind::InvalidValue { ty, value } => { + write!(f, "invalid value at line {line_number} for type {ty} ({value})") + } + ErrorKind::MalformedLine { line } => write!(f, "malformed line at line {line_number} ({line})"), + } + } +} + +pub fn load(properties: &mut PropertySet, input: &str) -> Result<(), Vec> { + let mut errors = Vec::new(); + + for (idx, line) in input.lines().enumerate() { + let mut split = line.splitn(3, ':'); + + if let (Some(key), Some(ty), Some(value)) = (split.next(), split.next(), split.next()) { + match ty { + "i" => { + if let Ok(value) = value.parse::() { + properties.insert(key.to_owned(), value); + } else { + errors.push(Error { + kind: ErrorKind::InvalidValue { + ty: ty.to_owned(), + value: value.to_owned(), + }, + line: idx, + }); + } + } + "s" => { + properties.insert(key.to_owned(), value); + } + _ => { + errors.push(Error { + kind: ErrorKind::UnknownType { ty: ty.to_owned() }, + line: idx, + }); + } + } + } else { + errors.push(Error { + kind: ErrorKind::MalformedLine { line: line.to_owned() }, + line: idx, + }) + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +pub struct ParseResult { + pub properties: PropertySet, + pub errors: Vec, +} + +pub fn parse(input: &str) -> ParseResult { + let mut properties = PropertySet::new(); + + let errors = match load(&mut properties, input) { + Ok(()) => Vec::new(), + Err(errors) => errors, + }; + + ParseResult { properties, errors } +} + +pub fn write(properties: &PropertySet) -> String { + let mut buf = String::new(); + + for (key, value) in properties.iter() { + buf.push_str(key); + + match value { + Value::Int(value) => { + buf.push_str(":i:"); + buf.push_str(&value.to_string()); + } + Value::Str(value) => { + buf.push_str(":s:"); + buf.push_str(value); + } + } + + buf.push('\n'); + } + + buf +} diff --git a/crates/ironrdp-rdpsnd-native/CHANGELOG.md b/crates/ironrdp-rdpsnd-native/CHANGELOG.md new file mode 100644 index 00000000..ff009f99 --- /dev/null +++ b/crates/ironrdp-rdpsnd-native/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.4.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.4.1...ironrdp-rdpsnd-native-v0.4.2)] - 2025-12-18 + +### Build + +- Bump bytemuck from 1.23.2 to 1.24.0 ([#1008](https://github.com/Devolutions/IronRDP/issues/1008)) ([a24a1fa9e8](https://github.com/Devolutions/IronRDP/commit/a24a1fa9e8f1898b2fcdd41d87660ab9e38f89ed)) + +## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.4.0...ironrdp-rdpsnd-native-v0.4.1)] - 2025-09-24 + +### Build + +- Replace `opus` by `opus2` (#985) ([e5042a7d81](https://github.com/Devolutions/IronRDP/commit/e5042a7d81b864e78ccf19d6b358d94458f951d0)) + + `opus` is unmaintained and points to a 4-year-old commit of the opus C + library. This does not compile anymore on our CI, because their + CMakeList.txt requires an older version of CMake that is not available + in the runners we use. `opus2` is a fork that points to a more recent + version of it. + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.3.1...ironrdp-rdpsnd-native-v0.4.0)] - 2025-08-29 + +### Build + +- Bump cpal to 0.16 ([eeac1fee1f](https://github.com/Devolutions/IronRDP/commit/eeac1fee1fed4858f4776d86072790bc074e34eb)) + +## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.3.0...ironrdp-rdpsnd-native-v0.3.1)] - 2025-06-27 + +### Build + +- Bump the patch group across 1 directory with 3 updates (#816) ([5c5f441bdd](https://github.com/Devolutions/IronRDP/commit/5c5f441bdd514d3fe6a29b4df872709167a9916d)) + +## [[0.1.4](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.1.3...ironrdp-rdpsnd-native-v0.1.4)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.1.2...ironrdp-rdpsnd-native-v0.1.3)] - 2025-02-05 + +### Features + +- Add Opus audio client decoding (#661) ([ccf6348270](https://github.com/Devolutions/IronRDP/commit/ccf63482706ecfbbdc6038028ea2ee086d0e3640)) + + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.1.1...ironrdp-rdpsnd-native-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.1.0...ironrdp-rdpsnd-native-v0.1.1)] - 2024-12-15 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-rdpsnd-native/Cargo.toml b/crates/ironrdp-rdpsnd-native/Cargo.toml new file mode 100644 index 00000000..c6711eb0 --- /dev/null +++ b/crates/ironrdp-rdpsnd-native/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "ironrdp-rdpsnd-native" +version = "0.4.2" +description = "Native RDPSND static channel backend implementations for IronRDP" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = ["opus"] +opus = ["dep:opus2", "dep:bytemuck"] + +[dependencies] +anyhow = "1" +bytemuck = { version = "1.24", optional = true } +cpal = "0.16" +ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.6" } # public +opus2 = { version = "0.3", optional = true, features = ["bundled"] } +tracing = { version = "0.1", features = ["log"] } + +[dev-dependencies] +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[lints] +workspace = true + diff --git a/crates/ironrdp-rdpsnd-native/LICENSE-APACHE b/crates/ironrdp-rdpsnd-native/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-rdpsnd-native/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-rdpsnd-native/LICENSE-MIT b/crates/ironrdp-rdpsnd-native/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-rdpsnd-native/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-rdpsnd-native/README.md b/crates/ironrdp-rdpsnd-native/README.md new file mode 100644 index 00000000..cde9477b --- /dev/null +++ b/crates/ironrdp-rdpsnd-native/README.md @@ -0,0 +1,10 @@ +# IronRDP RDPSND native backends + +Native RDPSND backend implementations. + +Currently, only [CPAL] backend is supported. + +This crate is part of the [IronRDP] project. + +[CPAL]: https://github.com/rustaudio/cpal +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-rdpsnd-native/examples/cpal.rs b/crates/ironrdp-rdpsnd-native/examples/cpal.rs new file mode 100644 index 00000000..2372e9d0 --- /dev/null +++ b/crates/ironrdp-rdpsnd-native/examples/cpal.rs @@ -0,0 +1,62 @@ +#![allow(unused_crate_dependencies)] // opus, false negative because it's a separate binary :/ + +use core::time::Duration; +use std::sync::mpsc; +use std::thread; + +use anyhow::Context as _; +use cpal::traits::StreamTrait as _; +use ironrdp_rdpsnd::pdu::{AudioFormat, WaveFormat}; +use ironrdp_rdpsnd_native::cpal::DecodeStream; +use tracing::debug; + +fn setup_logging() -> anyhow::Result<()> { + use tracing::metadata::LevelFilter; + use tracing_subscriber::prelude::*; + use tracing_subscriber::EnvFilter; + + let fmt_layer = tracing_subscriber::fmt::layer().compact(); + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .with_env_var("IRONRDP_LOG") + .from_env_lossy(); + + tracing_subscriber::registry() + .with(fmt_layer) + .with(env_filter) + .try_init() + .context("failed to set tracing global subscriber")?; + + Ok(()) +} + +fn main() -> anyhow::Result<()> { + setup_logging()?; + let rx_format = AudioFormat { + format: WaveFormat::PCM, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 88200, + n_block_align: 4, + bits_per_sample: 16, + data: None, + }; + let (tx, rx) = mpsc::channel(); + let stream = DecodeStream::new(&rx_format, rx)?; + + let producer = thread::spawn(move || { + let data_chunks = vec![vec![1u8, 2, 3], vec![4, 5, 6], vec![7, 8, 9]]; + for chunk in data_chunks { + tx.send(chunk).expect("failed to send data chunk"); + debug!("Sent a chunk"); + thread::sleep(Duration::from_secs(1)); // Simulating work + } + }); + + stream.stream().play()?; + thread::sleep(Duration::from_secs(3)); + let _ = producer.join(); + + Ok(()) +} diff --git a/crates/ironrdp-rdpsnd-native/src/cpal.rs b/crates/ironrdp-rdpsnd-native/src/cpal.rs new file mode 100644 index 00000000..f91d4719 --- /dev/null +++ b/crates/ironrdp-rdpsnd-native/src/cpal.rs @@ -0,0 +1,287 @@ +use core::sync::atomic::{AtomicBool, Ordering}; +use core::time::Duration; +use std::borrow::Cow; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::Arc; +use std::thread::{self, JoinHandle}; + +use anyhow::{bail, Context as _}; +use cpal::traits::{DeviceTrait as _, HostTrait as _}; +use cpal::{SampleFormat, Stream, StreamConfig}; +use ironrdp_rdpsnd::client::RdpsndClientHandler; +use ironrdp_rdpsnd::pdu::{AudioFormat, PitchPdu, VolumePdu, WaveFormat}; +use tracing::{debug, error, info, warn}; + +#[derive(Debug)] +pub struct RdpsndBackend { + // Unfortunately, Stream is not `Send`, so we move it to a separate thread. + stream_handle: Option>, + stream_ended: Arc, + tx: Option>>, + format_no: Option, +} + +impl Default for RdpsndBackend { + fn default() -> Self { + Self::new() + } +} + +impl RdpsndBackend { + pub fn new() -> Self { + Self { + tx: None, + format_no: None, + stream_handle: None, + stream_ended: Arc::new(AtomicBool::new(false)), + } + } +} + +impl Drop for RdpsndBackend { + fn drop(&mut self) { + self.close(); + } +} + +impl RdpsndClientHandler for RdpsndBackend { + fn get_formats(&self) -> &[AudioFormat] { + &[ + #[cfg(feature = "opus")] + AudioFormat { + format: WaveFormat::OPUS, + n_channels: 2, + n_samples_per_sec: 48000, + n_avg_bytes_per_sec: 192000, + n_block_align: 4, + bits_per_sample: 16, + data: None, + }, + AudioFormat { + format: WaveFormat::PCM, + n_channels: 2, + n_samples_per_sec: 44100, + n_avg_bytes_per_sec: 176400, + n_block_align: 4, + bits_per_sample: 16, + data: None, + }, + ] + } + + fn wave(&mut self, format_no: usize, _ts: u32, data: Cow<'_, [u8]>) { + if Some(format_no) != self.format_no { + debug!("New audio format"); + self.close(); + } + + if self.stream_handle.is_none() { + let (tx, rx) = mpsc::channel(); + self.tx = Some(tx); + + self.format_no = Some(format_no); + let Some(format) = self.get_formats().get(format_no) else { + warn!(?format_no, "Invalid format_no"); + return; + }; + let format = format.clone(); + self.stream_ended.store(false, Ordering::Relaxed); + let stream_ended = Arc::clone(&self.stream_ended); + self.stream_handle = Some(thread::spawn(move || { + let stream = match DecodeStream::new(&format, rx) { + Ok(stream) => stream, + Err(e) => { + error!(error = format!("{e:#}")); + return; + } + }; + debug!("Stream thread parking loop"); + while !stream_ended.load(Ordering::Relaxed) { + thread::park(); + } + debug!("Stream thread unparked"); + drop(stream); + })); + } + + if let Some(ref tx) = self.tx { + if let Err(error) = tx.send(data.to_vec()) { + error!(%error); + } + }; + } + + fn set_volume(&mut self, volume: VolumePdu) { + debug!(?volume); + } + + fn set_pitch(&mut self, pitch: PitchPdu) { + debug!(?pitch); + } + + fn close(&mut self) { + self.tx = None; + if let Some(stream) = self.stream_handle.take() { + self.stream_ended.store(true, Ordering::Relaxed); + stream.thread().unpark(); + if let Err(err) = stream.join() { + error!(?err, "Failed to join a stream thread"); + } + } + } +} + +#[doc(hidden)] +pub struct DecodeStream { + _dec_thread: Option>, + stream: Stream, +} + +impl DecodeStream { + pub fn new(rx_format: &AudioFormat, mut rx: Receiver>) -> anyhow::Result { + let mut dec_thread = None; + match rx_format.format { + #[cfg(feature = "opus")] + WaveFormat::OPUS => { + let chan = match rx_format.n_channels { + 1 => opus2::Channels::Mono, + 2 => opus2::Channels::Stereo, + _ => bail!("unsupported #channels for Opus"), + }; + let (dec_tx, dec_rx) = mpsc::channel(); + let mut dec = opus2::Decoder::new(rx_format.n_samples_per_sec, chan)?; + dec_thread = Some(thread::spawn(move || { + while let Ok(pkt) = rx.recv() { + let nb_samples = match dec.get_nb_samples(&pkt) { + Ok(nb_samples) => nb_samples, + Err(error) => { + error!(?error, "Failed to get the number of samples of an Opus packet"); + continue; + } + }; + + #[expect( + clippy::as_conversions, + reason = "opus::Channels has no conversions to usize implemented" + )] + let mut pcm = vec![0u8; nb_samples * chan as usize * size_of::()]; + if let Err(error) = dec.decode(&pkt, bytemuck::cast_slice_mut(pcm.as_mut_slice()), false) { + error!(?error, "Failed to decode an Opus packet"); + continue; + } + + if dec_tx.send(pcm).is_err() { + error!("Failed to send the decoded Opus packet over the channel"); + // If send has failed, it means that the receiver has been dropped. + // There is no point in continuing the loop in this case. + break; + } + } + })); + rx = dec_rx; + } + WaveFormat::PCM => {} + _ => bail!("audio format not supported"), + } + + let sample_format = match rx_format.bits_per_sample { + 8 => SampleFormat::U8, + 16 => SampleFormat::I16, + _ => { + bail!("only PCM 8/16 bits formats supported"); + } + }; + + let host = cpal::default_host(); + let device = host.default_output_device().context("no default output device")?; + let _supported_configs_range = device + .supported_output_configs() + .context("no supported output config")?; + let default_config = device.default_output_config()?; + debug!(?default_config); + + let mut rx = RxBuffer::new(rx); + let config = StreamConfig { + channels: rx_format.n_channels, + sample_rate: cpal::SampleRate(rx_format.n_samples_per_sec), + buffer_size: cpal::BufferSize::Default, + }; + debug!(?config); + + let stream = device + .build_output_stream_raw( + &config, + sample_format, + move |data, _info: &cpal::OutputCallbackInfo| { + let data = data.bytes_mut(); + rx.fill(data) + }, + |error| error!(%error), + None, + ) + .context("failed to setup output stream")?; + + Ok(Self { + _dec_thread: dec_thread, + stream, + }) + } + + pub fn stream(&self) -> &Stream { + &self.stream + } +} + +struct RxBuffer { + receiver: Receiver>, + last: Option>, + idx: usize, +} + +impl RxBuffer { + fn new(receiver: Receiver>) -> Self { + Self { + receiver, + last: None, + idx: 0, + } + } + + fn fill(&mut self, data: &mut [u8]) { + let mut filled = 0; + + while filled < data.len() { + if self.last.is_none() { + match self.receiver.recv_timeout(Duration::from_millis(4000)) { + Ok(rx) => { + debug!(rx.len = rx.len()); + self.last = Some(rx); + } + Err(error) => { + warn!(%error); + } + } + } + + let Some(ref last) = self.last else { + info!("Playback rx underrun"); + return; + }; + + #[expect(clippy::arithmetic_side_effects)] + while self.idx < last.len() && filled < data.len() { + data[filled] = last[self.idx]; + assert!(filled < usize::MAX); + assert!(self.idx < usize::MAX); + filled += 1; + self.idx += 1; + } + + // If all elements from last have been consumed, clear `self.last` + if self.idx >= last.len() { + self.last = None; + self.idx = 0; + } + } + } +} diff --git a/crates/ironrdp-rdpsnd-native/src/lib.rs b/crates/ironrdp-rdpsnd-native/src/lib.rs new file mode 100644 index 00000000..04a3b3cf --- /dev/null +++ b/crates/ironrdp-rdpsnd-native/src/lib.rs @@ -0,0 +1,7 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +#[cfg(test)] +use tracing_subscriber as _; + +pub mod cpal; diff --git a/crates/ironrdp-rdpsnd/CHANGELOG.md b/crates/ironrdp-rdpsnd/CHANGELOG.md new file mode 100644 index 00000000..d8a6796a --- /dev/null +++ b/crates/ironrdp-rdpsnd/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-v0.4.0...ironrdp-rdpsnd-v0.5.0)] - 2025-05-27 + +### Features + +- Add support for client custom flags ([7bd92c0ce5](https://github.com/Devolutions/IronRDP/commit/7bd92c0ce5c686fe18c062b7edfeed46a709fc23)) + + Client can support various flags, but always set ALIVE. + +### Bug Fixes + +- Correct TrainingPdu wPackSize field ([abcc42e01f](https://github.com/Devolutions/IronRDP/commit/abcc42e01fda3ce9c8e1739524e0fc73b8778d83)) + +- Reply to TrainingPdu ([5dcc526f51](https://github.com/Devolutions/IronRDP/commit/5dcc526f513e8083ff335cad3cc80d2effeb7265)) + +- Lookup the associated format from the client list ([3d7bc28b97](https://github.com/Devolutions/IronRDP/commit/3d7bc28b9764b1f37b038bb2fbb676ec464ee5ee)) + +- Send client formats that match server (#742) ([a8b9614323](https://github.com/Devolutions/IronRDP/commit/a8b96143236ad457b5241f6a2f8acfaf969472b6)) + + Windows seems to be confused if the client replies with more formats, or unknown formats (opus). + +### Refactor + +- [**breaking**] Pass format_no instead of AudioFormat ([4172571e8e](https://github.com/Devolutions/IronRDP/commit/4172571e8e061a6a120643393881b5e37f1e61ab)) + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-v0.3.1...ironrdp-rdpsnd-v0.4.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-v0.3.0...ironrdp-rdpsnd-v0.3.1)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-v0.2.0...ironrdp-rdpsnd-v0.3.0)] - 2025-02-05 + +### Features + +- New required method `get_formats` for the `RdpsndClientHandler` trait (#661) ([ccf6348270](https://github.com/Devolutions/IronRDP/commit/ccf63482706ecfbbdc6038028ea2ee086d0e3640)) + + + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-v0.1.1...ironrdp-rdpsnd-v0.2.0)] - 2025-01-28 + +### Features + +- Support for volume setting (#641) ([a6c36511f6](https://github.com/Devolutions/IronRDP/commit/a6c36511f6584f67b8c6e795c34d5007ec2b24a4)) + + Add server messages and API to support setting client volume. + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-v0.1.0...ironrdp-rdpsnd-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-rdpsnd/Cargo.toml b/crates/ironrdp-rdpsnd/Cargo.toml new file mode 100644 index 00000000..4f4b6e5b --- /dev/null +++ b/crates/ironrdp-rdpsnd/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "ironrdp-rdpsnd" +version = "0.6.0" +readme = "README.md" +description = "RDPSND static channel for audio output implemented as described in MS-RDPEA" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +std = [] + +[dependencies] +bitflags = "2.9" +tracing = { version = "0.1", features = ["log"] } +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6", features = ["alloc"] } # public + +[lints] +workspace = true diff --git a/crates/ironrdp-rdpsnd/LICENSE-APACHE b/crates/ironrdp-rdpsnd/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-rdpsnd/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-rdpsnd/LICENSE-MIT b/crates/ironrdp-rdpsnd/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-rdpsnd/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-rdpsnd/README.md b/crates/ironrdp-rdpsnd/README.md new file mode 100644 index 00000000..89c54489 --- /dev/null +++ b/crates/ironrdp-rdpsnd/README.md @@ -0,0 +1,8 @@ +# IronRDP RDPSND + +RDPSND static channel for audio output implemented as described in [MS-RDPEA]. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP +[MS-RDPEA]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpea/bea2d5cf-e3b9-4419-92e5-0e074ff9bc5b diff --git a/crates/ironrdp-rdpsnd/src/client.rs b/crates/ironrdp-rdpsnd/src/client.rs new file mode 100644 index 00000000..0cc8c6ed --- /dev/null +++ b/crates/ironrdp-rdpsnd/src/client.rs @@ -0,0 +1,238 @@ +use std::borrow::Cow; +use std::collections::HashSet; + +use ironrdp_core::{cast_length, impl_as_any, Decode as _, EncodeResult, ReadCursor}; +use ironrdp_pdu::gcc::ChannelName; +use ironrdp_pdu::{decode_err, encode_err, pdu_other_err, PduResult}; +use ironrdp_svc::{CompressionCondition, SvcClientProcessor, SvcMessage, SvcProcessor}; +use tracing::{debug, error}; + +use crate::pdu::{self, AudioFormat, PitchPdu, ServerAudioFormatPdu, TrainingPdu, VolumePdu}; +use crate::server::RdpsndSvcMessages; + +pub trait RdpsndClientHandler: Send + core::fmt::Debug { + fn get_flags(&self) -> pdu::AudioFormatFlags { + pdu::AudioFormatFlags::empty() + } + + fn get_formats(&self) -> &[AudioFormat]; + + fn wave(&mut self, format_no: usize, ts: u32, data: Cow<'_, [u8]>); + + fn set_volume(&mut self, volume: VolumePdu); + + fn set_pitch(&mut self, pitch: PitchPdu); + + fn close(&mut self); +} + +#[derive(Debug)] +pub struct NoopRdpsndBackend; + +impl RdpsndClientHandler for NoopRdpsndBackend { + fn get_formats(&self) -> &[AudioFormat] { + &[] + } + + fn wave(&mut self, _format_no: usize, _ts: u32, _data: Cow<'_, [u8]>) {} + + fn set_volume(&mut self, _volume: VolumePdu) {} + + fn set_pitch(&mut self, _pitch: PitchPdu) {} + + fn close(&mut self) {} +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum RdpsndState { + Start, + WaitingForTraining, + Ready, + Stop, +} + +/// Required for rdpdr to work: [\[MS-RDPEFS\] Appendix A<1>] +/// +/// [\[MS-RDPEFS\] Appendix A<1>]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/fd28bfd9-dae2-4a78-abe1-b4efa208b7aa#Appendix_A_1 +#[derive(Debug)] +pub struct Rdpsnd { + handler: Box, + state: RdpsndState, + server_format: Option, +} + +impl Rdpsnd { + pub const NAME: ChannelName = ChannelName::from_static(b"rdpsnd\0\0"); + + pub fn new(handler: Box) -> Self { + Self { + handler, + state: RdpsndState::Start, + server_format: None, + } + } + + pub fn get_format(&self, format_no: u16) -> PduResult<&AudioFormat> { + let server_format = self + .server_format + .as_ref() + .ok_or_else(|| pdu_other_err!("invalid state - no format"))?; + + server_format + .formats + .get(usize::from(format_no)) + .ok_or_else(|| pdu_other_err!("invalid format")) + } + + pub fn version(&self) -> PduResult { + let server_format = self + .server_format + .as_ref() + .ok_or_else(|| pdu_other_err!("invalid state - no version"))?; + + Ok(server_format.version) + } + + pub fn client_formats(&mut self) -> PduResult { + // Windows seems to be confused if the client replies with more formats, or unknown formats (e.g.: opus). + // We ensure to only send supported formats in common with the server. + let server_format: HashSet<_> = self + .server_format + .as_ref() + .ok_or_else(|| pdu_other_err!("invalid state - no server format"))? + .formats + .iter() + .collect(); + let formats: HashSet<_> = self.handler.get_formats().iter().collect(); + let formats = formats.intersection(&server_format).map(|&x| x.clone()).collect(); + + let pdu = pdu::ClientAudioFormatPdu { + version: self.version()?, + flags: self.handler.get_flags() | pdu::AudioFormatFlags::ALIVE, + formats, + volume_left: 0xFFFF, + volume_right: 0xFFFF, + pitch: 0x00010000, + dgram_port: 0, + }; + Ok(RdpsndSvcMessages::new(vec![pdu::ClientAudioOutputPdu::AudioFormat( + pdu, + ) + .into()])) + } + + pub fn quality_mode(&mut self) -> PduResult { + let pdu = pdu::QualityModePdu { + quality_mode: pdu::QualityMode::High, + }; + Ok(RdpsndSvcMessages::new(vec![pdu::ClientAudioOutputPdu::QualityMode( + pdu, + ) + .into()])) + } + + pub fn training_confirm(&mut self, pdu: &TrainingPdu) -> PduResult { + let pack_size: EncodeResult<_> = cast_length!("wPackSize", pdu.data.len()); + let pack_size = pack_size.map_err(|e| encode_err!(e))?; + let pdu = pdu::TrainingConfirmPdu { + timestamp: pdu.timestamp, + pack_size, + }; + Ok(RdpsndSvcMessages::new(vec![ + pdu::ClientAudioOutputPdu::TrainingConfirm(pdu).into(), + ])) + } + + pub fn wave_confirm(&mut self, timestamp: u16, block_no: u8) -> PduResult { + let pdu = pdu::WaveConfirmPdu { timestamp, block_no }; + Ok(RdpsndSvcMessages::new(vec![pdu::ClientAudioOutputPdu::WaveConfirm( + pdu, + ) + .into()])) + } +} + +impl_as_any!(Rdpsnd); + +impl SvcProcessor for Rdpsnd { + fn channel_name(&self) -> ChannelName { + Self::NAME + } + + fn compression_condition(&self) -> CompressionCondition { + CompressionCondition::Never + } + + fn process(&mut self, payload: &[u8]) -> PduResult> { + let pdu = pdu::ServerAudioOutputPdu::decode(&mut ReadCursor::new(payload)).map_err(|e| decode_err!(e))?; + + debug!(?pdu, ?self.state); + let msg = match self.state { + RdpsndState::Start => { + let pdu::ServerAudioOutputPdu::AudioFormat(af) = pdu else { + error!("Invalid pdu"); + self.state = RdpsndState::Stop; + return Ok(vec![]); + }; + self.server_format = Some(af); + self.state = RdpsndState::WaitingForTraining; + let mut msgs: Vec = self.client_formats()?.into(); + if self.version()? >= pdu::Version::V6 { + let mut m = self.quality_mode()?.into(); + msgs.append(&mut m); + } + msgs + } + RdpsndState::WaitingForTraining => { + let pdu::ServerAudioOutputPdu::Training(pdu) = pdu else { + error!("Invalid PDU"); + self.state = RdpsndState::Stop; + return Ok(vec![]); + }; + self.state = RdpsndState::Ready; + self.training_confirm(&pdu)?.into() + } + RdpsndState::Ready => { + match pdu { + // TODO: handle WaveInfo for < v8 + pdu::ServerAudioOutputPdu::Wave2(pdu) => { + let format_no = usize::from(pdu.format_no); + let ts = pdu.audio_timestamp; + self.handler.wave(format_no, ts, pdu.data); + return Ok(self.wave_confirm(pdu.timestamp, pdu.block_no)?.into()); + } + pdu::ServerAudioOutputPdu::Volume(pdu) => { + self.handler.set_volume(pdu); + } + pdu::ServerAudioOutputPdu::Pitch(pdu) => { + self.handler.set_pitch(pdu); + } + pdu::ServerAudioOutputPdu::Close => { + self.handler.close(); + } + pdu::ServerAudioOutputPdu::Training(pdu) => return Ok(self.training_confirm(&pdu)?.into()), + _ => { + error!("Invalid PDU"); + self.state = RdpsndState::Stop; + return Ok(vec![]); + } + } + vec![] + } + state => { + error!(?state, "Invalid state"); + vec![] + } + }; + + Ok(msg) + } +} + +impl Drop for Rdpsnd { + fn drop(&mut self) { + self.handler.close(); + } +} + +impl SvcClientProcessor for Rdpsnd {} diff --git a/crates/ironrdp-rdpsnd/src/lib.rs b/crates/ironrdp-rdpsnd/src/lib.rs new file mode 100644 index 00000000..55b0f6dd --- /dev/null +++ b/crates/ironrdp-rdpsnd/src/lib.rs @@ -0,0 +1,6 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +pub mod client; +pub mod pdu; +pub mod server; diff --git a/crates/ironrdp-rdpsnd/src/pdu/mod.rs b/crates/ironrdp-rdpsnd/src/pdu/mod.rs new file mode 100644 index 00000000..258b239a --- /dev/null +++ b/crates/ironrdp-rdpsnd/src/pdu/mod.rs @@ -0,0 +1,1366 @@ +//! Audio Output Virtual Channel Extension PDUs [MS-RDPEA][1] implementation. +//! +//! [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpea/bea2d5cf-e3b9-4419-92e5-0e074ff9bc5b + +use std::borrow::Cow; +use std::fmt; + +use bitflags::bitflags; +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, other_err, Decode, DecodeError, DecodeResult, + Encode, EncodeResult, ReadCursor, WriteCursor, +}; +use ironrdp_pdu::{read_padding, write_padding}; +use ironrdp_svc::SvcEncode; + +const SNDC_FORMATS: u8 = 0x07; +const SNDC_QUALITYMODE: u8 = 0x0C; +const SNDC_CRYPTKEY: u8 = 0x08; +const SNDC_TRAINING: u8 = 0x06; +const SNDC_WAVE: u8 = 0x02; +const SNDC_WAVECONFIRM: u8 = 0x05; +const SNDC_WAVEENCRYPT: u8 = 0x09; +const SNDC_CLOSE: u8 = 0x01; +const SNDC_WAVE2: u8 = 0x0D; +const SNDC_VOLUME: u8 = 0x03; +const SNDC_PITCH: u8 = 0x04; + +// TODO: UDP PDUs + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq)] +pub enum Version { + V2 = 0x02, + V5 = 0x05, + V6 = 0x06, + V8 = 0x08, +} + +impl TryFrom for Version { + type Error = DecodeError; + + fn try_from(value: u16) -> Result { + match value { + 0x02 => Ok(Self::V2), + 0x05 => Ok(Self::V5), + 0x06 => Ok(Self::V6), + 0x08 => Ok(Self::V8), + _ => Err(invalid_field_err!("Version", "unknown audio output version")), + } + } +} + +impl From for u16 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(version: Version) -> Self { + version as u16 + } +} + +// format tag: +// http://tools.ietf.org/html/rfc2361 +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WaveFormat(u16); + +macro_rules! wave_formats { + ( + $( + ($konst:ident, $num:expr); + )+ + ) => { + impl WaveFormat { + $( + pub const $konst: WaveFormat = WaveFormat($num); + )+ + + fn as_str(&self) -> Option<&'static str> { + match self.0 { + $( + $num => Some(stringify!($konst)), + )+ + _ => None + } + } + } + } +} + +wave_formats! { + (UNKNOWN, 0x0000); + (PCM, 0x0001); + (ADPCM, 0x0002); + (IEEE_FLOAT, 0x0003); + (VSELP, 0x0004); + (IBM_CVSD, 0x0005); + (ALAW, 0x0006); + (MULAW, 0x0007); + (OKI_ADPCM, 0x0010); + (DVI_ADPCM, 0x0011); + (MEDIASPACE_ADPCM, 0x0012); + (SIERRA_ADPCM, 0x0013); + (G723_ADPCM, 0x0014); + (DIGISTD, 0x0015); + (DIGIFIX, 0x0016); + (DIALOGIC_OKI_ADPCM, 0x0017); + (MEDIAVISION_ADPCM, 0x0018); + (CU_CODEC, 0x0019); + (YAMAHA_ADPCM, 0x0020); + (SONARC, 0x0021); + (DSPGROUP_TRUESPEECH, 0x0022); + (ECHOSC1, 0x0023); + (AUDIOFILE_AF36, 0x0024); + (APTX, 0x0025); + (AUDIOFILE_AF10, 0x0026); + (PROSODY_1612, 0x0027); + (LRC, 0x0028); + (DOLBY_AC2, 0x0030); + (GSM610, 0x0031); + (MSNAUDIO, 0x0032); + (ANTEX_ADPCME, 0x0033); + (CONTROL_RES_VQLPC, 0x0034); + (DIGIREAL, 0x0035); + (DIGIADPCM, 0x0036); + (CONTROL_RES_CR10, 0x0037); + (NMS_VBXADPCM, 0x0038); + (ROLAND_RDAC, 0x0039); + (ECHOSC3, 0x003A); + (ROCKWELL_ADPCM, 0x003B); + (ROCKWELL_DIGITALK, 0x003C); + (XEBEC, 0x003D); + (G721_ADPCM, 0x0040); + (G728_CELP, 0x0041); + (MSG723, 0x0042); + (MPEG, 0x0050); + (RT24, 0x0052); + (PAC, 0x0053); + (MPEGLAYER3, 0x0055); + (LUCENT_G723, 0x0059); + (CIRRUS, 0x0060); + (ESPCM, 0x0061); + (VOXWARE, 0x0062); + (CANOPUS_ATRAC, 0x0063); + (G726_ADPCM, 0x0064); + (G722_ADPCM, 0x0065); + (DSAT, 0x0066); + (DSAT_DISPLAY, 0x0067); + (VOXWARE_BYTE_ALIGNED, 0x0069); + (VOXWARE_AC8, 0x0070); + (VOXWARE_AC10, 0x0071); + (VOXWARE_AC16, 0x0072); + (VOXWARE_AC20, 0x0073); + (VOXWARE_RT24, 0x0074); + (VOXWARE_RT29, 0x0075); + (VOXWARE_RT29HW, 0x0076); + (VOXWARE_VR12, 0x0077); + (VOXWARE_VR18, 0x0078); + (VOXWARE_TQ40, 0x0079); + (SOFTSOUND, 0x0080); + (VOXWARE_TQ60, 0x0081); + (MSRT24, 0x0082); + (G729A, 0x0083); + (MVI_MV12, 0x0084); + (DF_G726, 0x0085); + (DF_GSM610, 0x0086); + (ISIAUDIO, 0x0088); + (ONLIVE, 0x0089); + (SBC24, 0x0091); + (DOLBY_AC3_SPDIF, 0x0092); + (ZYXEL_ADPCM, 0x0097); + (PHILIPS_LPCBB, 0x0098); + (PACKED, 0x0099); + (RHETOREX_ADPCM, 0x0100); + (IRAT, 0x0101); + (VIVO_G723, 0x0111); + (VIVO_SIREN, 0x0112); + (DIGITAL_G723, 0x0123); + (WMAUDIO2, 0x0161); + (WMAUDIO3, 0x0162); + (WMAUDIO_LOSSLESS, 0x0163); + (CREATIVE_ADPCM, 0x0200); + (CREATIVE_FASTSPEECH8, 0x0202); + (CREATIVE_FASTSPEECH10, 0x0203); + (QUARTERDECK, 0x0220); + (FM_TOWNS_SND, 0x0300); + (BTV_DIGITAL, 0x0400); + (VME_VMPCM, 0x0680); + (OLIGSM, 0x1000); + (OLIADPCM, 0x1001); + (OLICELP, 0x1002); + (OLISBC, 0x1003); + (OLIOPR, 0x1004); + (LH_CODEC, 0x1100); + (NORRIS, 0x1400); + (SOUNDSPACE_MUSICOMPRESS, 0x1500); + (DVM, 0x2000); + (OPUS, 0x704F); + (AAC_MS, 0xA106); + (EXTENSIBLE, 0xFFFE); +} + +impl fmt::Debug for WaveFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl fmt::Display for WaveFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.0, self.as_str().unwrap_or("")) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AudioFormat { + pub format: WaveFormat, + pub n_channels: u16, + pub n_samples_per_sec: u32, + pub n_avg_bytes_per_sec: u32, + pub n_block_align: u16, + pub bits_per_sample: u16, + pub data: Option>, +} + +impl AudioFormat { + const NAME: &'static str = "SERVER_AUDIO_VERSION_AND_FORMATS"; + + const FIXED_PART_SIZE: usize = + 2 /* wFormatTag */ + + 2 /* nChannels */ + + 4 /* nSamplesPerSec */ + + 4 /* nAvgBytesPerSec */ + + 2 /* nBlockAlign */ + + 2 /* wBitsPerSample */ + + 2 /* cbSize */; +} + +impl Encode for AudioFormat { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.format.0); + dst.write_u16(self.n_channels); + dst.write_u32(self.n_samples_per_sec); + dst.write_u32(self.n_avg_bytes_per_sec); + dst.write_u16(self.n_block_align); + dst.write_u16(self.bits_per_sample); + let len = self.data.as_ref().map_or(0, |d| d.len()); + dst.write_u16(cast_length!("AudioFormat::cbSize", len)?); + if let Some(data) = &self.data { + dst.write_slice(data); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(self.data.as_ref().map_or(0, |d| d.len())) + .expect("never overflow") + } +} + +impl<'de> Decode<'de> for AudioFormat { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let format = WaveFormat(src.read_u16()); + let n_channels = src.read_u16(); + let n_samples_per_sec = src.read_u32(); + let n_avg_bytes_per_sec = src.read_u32(); + let n_block_align = src.read_u16(); + let bits_per_sample = src.read_u16(); + let cb_size = cast_length!("cbSize", src.read_u16())?; + + ensure_size!(in: src, size: cb_size); + let data = if cb_size > 0 { + Some(src.read_slice(cb_size).into()) + } else { + None + }; + + Ok(Self { + format, + n_channels, + n_samples_per_sec, + n_avg_bytes_per_sec, + n_block_align, + bits_per_sample, + data, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerAudioFormatPdu { + pub version: Version, + pub formats: Vec, +} + +impl ServerAudioFormatPdu { + const NAME: &'static str = "SERVER_AUDIO_VERSION_AND_FORMATS"; + + const FIXED_PART_SIZE: usize = + 4 /* dwFlags */ + + 4 /* dwVolume */ + + 4 /* dwPitch */ + + 2 /* wDGramPort */ + + 2 /* wNumberOfFormats */ + + 1 /* cLastBlockConfirmed */ + + 2 /* wVersion */ + + 1 /* bPad */; +} + +impl Encode for ServerAudioFormatPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + write_padding!(dst, 4); /* flags */ + write_padding!(dst, 4); /* volume */ + write_padding!(dst, 4); /* pitch */ + write_padding!(dst, 2); /* DGramPort */ + dst.write_u16(cast_length!("AudioFormatPdu::n_formats", self.formats.len())?); + write_padding!(dst, 1); /* blockNo */ + dst.write_u16(self.version.into()); + write_padding!(dst, 1); + for fmt in self.formats.iter() { + fmt.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(self.formats.iter().map(|fmt| fmt.size()).sum::()) + .expect("never overflow") + } +} + +impl<'de> Decode<'de> for ServerAudioFormatPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + read_padding!(src, 4); /* flags */ + read_padding!(src, 4); /* volume */ + read_padding!(src, 4); /* pitch */ + read_padding!(src, 2); /* DGramPort */ + let n_formats = usize::from(src.read_u16()); + read_padding!(src, 1); /* blockNo */ + let version = Version::try_from(src.read_u16())?; + read_padding!(src, 1); + let formats = core::iter::repeat_with(|| AudioFormat::decode(src)) + .take(n_formats) + .collect::>()?; + + Ok(Self { version, formats }) + } +} + +bitflags! { + /// Represents `dwFlags` field of `CLIENT_AUDIO_VERSION_AND_FORMATS` structure. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct AudioFormatFlags: u32 { + /// The client is capable of consuming audio data. This flag MUST be set + /// for audio data to be transferred. + const ALIVE = 0x0000_0001; + /// The client is capable of applying a volume change to all the audio + /// data that is received. + const VOLUME = 0x0000_0002; + /// The client is capable of applying a pitch change to all the audio + /// data that is received. + const PITCH = 0x0000_00004; + // The source may set any bits + const _ = !0; + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientAudioFormatPdu { + pub version: Version, + pub flags: AudioFormatFlags, + pub formats: Vec, + pub volume_left: u16, + pub volume_right: u16, + pub pitch: u32, + pub dgram_port: u16, +} + +impl ClientAudioFormatPdu { + const NAME: &'static str = "CLIENT_AUDIO_VERSION_AND_FORMATS"; + + const FIXED_PART_SIZE: usize = + 4 /* dwFlags */ + + 4 /* dwVolume */ + + 4 /* dwPitch */ + + 2 /* wDGramPort */ + + 2 /* wNumberOfFormats */ + + 1 /* cLastBlockConfirmed */ + + 2 /* wVersion */ + + 1 /* bPad */; +} + +impl Encode for ClientAudioFormatPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.flags.bits()); + let volume = (u32::from(self.volume_right) << 16) | u32::from(self.volume_left); + dst.write_u32(volume); + dst.write_u32(self.pitch); + dst.write_u16_be(self.dgram_port); + dst.write_u16(cast_length!("AudioFormatPdu::n_formats", self.formats.len())?); + dst.write_u8(0); /* blockNo */ + dst.write_u16(self.version.into()); + write_padding!(dst, 1); + for fmt in self.formats.iter() { + fmt.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(self.formats.iter().map(|fmt| fmt.size()).sum::()) + .expect("never overflow") + } +} + +impl<'de> Decode<'de> for ClientAudioFormatPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = AudioFormatFlags::from_bits_truncate(src.read_u32()); + let volume_left = src.read_u16(); + let volume_right = src.read_u16(); + let pitch = src.read_u32(); + let dgram_port = src.read_u16_be(); + let n_formats = usize::from(src.read_u16()); + let _block_no = src.read_u8(); + let version = Version::try_from(src.read_u16())?; + read_padding!(src, 1); + let formats = core::iter::repeat_with(|| AudioFormat::decode(src)) + .take(n_formats) + .collect::>()?; + + Ok(Self { + version, + flags, + formats, + volume_left, + volume_right, + pitch, + dgram_port, + }) + } +} + +#[repr(u16)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum QualityMode { + Dynamic = 0x00, + Medium = 0x01, + High = 0x02, +} + +impl TryFrom for QualityMode { + type Error = DecodeError; + + fn try_from(value: u16) -> Result { + match value { + 0x00 => Ok(Self::Dynamic), + 0x01 => Ok(Self::Medium), + 0x02 => Ok(Self::High), + _ => Err(invalid_field_err!("QualityMode", "unknown audio quality mode")), + } + } +} + +impl From for u16 { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn from(mode: QualityMode) -> Self { + mode as u16 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct QualityModePdu { + pub quality_mode: QualityMode, +} + +impl QualityModePdu { + const NAME: &'static str = "AUDIO_QUALITY_MODE"; + + const FIXED_PART_SIZE: usize = + 2 /* wQualityMode */ + + 2 /* reserved */; +} + +impl Encode for QualityModePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.quality_mode.into()); + write_padding!(dst, 2); /* reserved */ + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for QualityModePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let quality_mode = QualityMode::try_from(src.read_u16())?; + read_padding!(src, 2); /* reserved */ + + Ok(Self { quality_mode }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CryptKeyPdu { + pub seed: [u8; 32], +} + +impl CryptKeyPdu { + const NAME: &'static str = "SNDCRYPT"; + + const FIXED_PART_SIZE: usize = + 4 /* reserved */ + + 32 /* seed */; +} + +impl Encode for CryptKeyPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + write_padding!(dst, 4); /* reserved */ + dst.write_array(self.seed); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for CryptKeyPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + read_padding!(src, 4); /* reserved */ + let seed = src.read_array(); + + Ok(Self { seed }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TrainingPdu { + pub timestamp: u16, + pub data: Vec, +} + +impl TrainingPdu { + const NAME: &'static str = "SNDTRAINING"; + + const FIXED_PART_SIZE: usize = + 2 /* wTimeStamp */ + + 2 /* wPackSize */; +} + +impl Encode for TrainingPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.timestamp); + let len = if self.data.is_empty() { + 0 + } else { + self.size() + ServerAudioOutputPdu::FIXED_PART_SIZE + }; + dst.write_u16(cast_length!("TrainingPdu::wPackSize", len)?); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(self.data.len()) + .expect("never overflow") + } +} + +impl<'de> Decode<'de> for TrainingPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let timestamp = src.read_u16(); + let len = usize::from(src.read_u16()); + let data = if len != 0 { + if len < Self::FIXED_PART_SIZE + ServerAudioOutputPdu::FIXED_PART_SIZE { + return Err(invalid_field_err!("TrainingPdu::wPackSize", "too small")); + } + let len = len - Self::FIXED_PART_SIZE - ServerAudioOutputPdu::FIXED_PART_SIZE; + ensure_size!(in: src, size: len); + src.read_slice(len).into() + } else { + Vec::new() + }; + + Ok(Self { timestamp, data }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TrainingConfirmPdu { + pub timestamp: u16, + pub pack_size: u16, +} + +impl TrainingConfirmPdu { + const NAME: &'static str = "SNDTRAININGCONFIRM"; + + const FIXED_PART_SIZE: usize = + 2 /* wTimeStamp */ + + 2 /* wPackSize */; +} + +impl Encode for TrainingConfirmPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.timestamp); + dst.write_u16(self.pack_size); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for TrainingConfirmPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let timestamp = src.read_u16(); + let pack_size = src.read_u16(); + + Ok(Self { timestamp, pack_size }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WaveInfoPdu { + pub timestamp: u16, + pub format_no: u16, + pub block_no: u8, + pub data: [u8; 4], +} + +impl WaveInfoPdu { + const NAME: &'static str = "SNDWAVEINFO"; + + const FIXED_PART_SIZE: usize = + 2 /* wTimeStamp */ + + 2 /* wFormatNo */ + + 1 /* cBlockNo */ + + 3 /* bPad */ + + 4 /* data */; +} + +impl Encode for WaveInfoPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.timestamp); + dst.write_u16(self.format_no); + dst.write_u8(self.block_no); + write_padding!(dst, 3); + dst.write_array(self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for WaveInfoPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let timestamp = src.read_u16(); + let format_no = src.read_u16(); + let block_no = src.read_u8(); + read_padding!(src, 3); + let data = src.read_array(); + + Ok(Self { + timestamp, + format_no, + block_no, + data, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SndWavePdu { + pub data: Vec, +} + +impl SndWavePdu { + const NAME: &'static str = "SNDWAVE"; + + const FIXED_PART_SIZE: usize = 4 /* bPad */; + + fn decode(src: &mut ReadCursor<'_>, data_len: usize) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + read_padding!(src, 4); + ensure_size!(in: src, size: data_len); + let data = src.read_slice(data_len).into(); + + Ok(Self { data }) + } +} + +impl Encode for SndWavePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + write_padding!(dst, 4); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(self.data.len()) + .expect("never overflow") + } +} + +// combines WaveInfoPdu + WavePdu +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WavePdu<'a> { + pub timestamp: u16, + pub format_no: u16, + pub block_no: u8, + pub data: Cow<'a, [u8]>, +} + +impl WavePdu<'_> { + const NAME: &'static str = "WavePdu"; + + fn body_size(&self) -> usize { + (WaveInfoPdu::FIXED_PART_SIZE - 4) + .checked_add(self.data.len()) + .expect("never overflow") + } +} + +impl Encode for WavePdu<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let info = WaveInfoPdu { + timestamp: self.timestamp, + format_no: self.format_no, + block_no: self.block_no, + data: self.data[0..4] + .try_into() + .map_err(|e| other_err!("invalid data", source: e))?, + }; + let wave = SndWavePdu { + data: self.data[4..].into(), + }; + info.encode(dst)?; + wave.encode(dst)?; + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + (WaveInfoPdu::FIXED_PART_SIZE + SndWavePdu::FIXED_PART_SIZE - 4) + .checked_add(self.data.len()) + .expect("never overflow") + } +} + +impl WavePdu<'_> { + fn decode(src: &mut ReadCursor<'_>, body_size: u16) -> DecodeResult { + let info = WaveInfoPdu::decode(src)?; + let body_size = usize::from(body_size); + let data_len = body_size + .checked_sub(info.size()) + .ok_or_else(|| invalid_field_err!("Length", "WaveInfo body_size is too small"))?; + let wave = SndWavePdu::decode(src, data_len)?; + + let mut data = Vec::with_capacity(wave.size()); + data.extend_from_slice(&info.data); + data.extend_from_slice(&wave.data); + + Ok(Self { + timestamp: info.timestamp, + format_no: info.format_no, + block_no: info.block_no, + data: data.into(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WaveConfirmPdu { + pub timestamp: u16, + pub block_no: u8, +} + +impl WaveConfirmPdu { + const NAME: &'static str = "SNDWAV_CONFIRM"; + + const FIXED_PART_SIZE: usize = + 2 /* wTimeStamp */ + + 1 /* cConfirmBlockNo */ + + 1 /* pad */; +} + +impl Encode for WaveConfirmPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.timestamp); + dst.write_u8(self.block_no); + write_padding!(dst, 1); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for WaveConfirmPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let timestamp = src.read_u16(); + let block_no = src.read_u8(); + read_padding!(src, 1); + + Ok(Self { timestamp, block_no }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WaveEncryptPdu { + pub timestamp: u16, + pub format_no: u16, + pub block_no: u8, + pub signature: Option<[u8; 8]>, + // TODO: use Cow? + pub data: Vec, +} + +impl WaveEncryptPdu { + const NAME: &'static str = "SNDWAVECRYPT"; + + const FIXED_PART_SIZE: usize = + 2 /* wTimeStamp */ + + 2 /* wFormatNo */ + + 1 /* cBlockNo */ + + 3 /* bPad */; + + fn decode(src: &mut ReadCursor<'_>, version: Version) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let timestamp = src.read_u16(); + let format_no = src.read_u16(); + let block_no = src.read_u8(); + read_padding!(src, 3); + let signature = if version >= Version::V5 { + ensure_size!(in: src, size: 8); + Some(src.read_array()) + } else { + None + }; + let data = src.read_remaining().into(); + + Ok(Self { + timestamp, + format_no, + block_no, + signature, + data, + }) + } +} + +impl Encode for WaveEncryptPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.timestamp); + dst.write_u16(self.format_no); + dst.write_u8(self.block_no); + write_padding!(dst, 3); + if let Some(sig) = self.signature.as_ref() { + dst.write_slice(sig); + } + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(self.signature.map_or(0, |_| 8)) + .expect("never overflow") + .checked_add(self.data.len()) + .expect("never overflow") + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct Wave2Pdu<'a> { + pub timestamp: u16, + pub format_no: u16, + pub block_no: u8, + pub audio_timestamp: u32, + pub data: Cow<'a, [u8]>, +} + +impl fmt::Debug for Wave2Pdu<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Wave2Pdu") + .field("timestamp", &self.timestamp) + .field("format_no", &self.format_no) + .field("block_no", &self.block_no) + .field("audio_timestamp", &self.audio_timestamp) + .finish() + } +} + +impl Wave2Pdu<'_> { + const NAME: &'static str = "SNDWAVE2"; + + const FIXED_PART_SIZE: usize = + 2 /* wTimeStamp */ + + 2 /* wFormatNo */ + + 1 /* cBlockNo */ + + 3 /* bPad */ + + 4 /* dwAudioTimestamp */; +} + +impl Encode for Wave2Pdu<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.timestamp); + dst.write_u16(self.format_no); + dst.write_u8(self.block_no); + write_padding!(dst, 3); + dst.write_u32(self.audio_timestamp); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(self.data.len()) + .expect("never overflow") + } +} + +impl<'de> Decode<'de> for Wave2Pdu<'_> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let timestamp = src.read_u16(); + let format_no = src.read_u16(); + let block_no = src.read_u8(); + read_padding!(src, 3); + let audio_timestamp = src.read_u32(); + let data = src.read_remaining().to_vec().into(); + + Ok(Self { + timestamp, + format_no, + block_no, + audio_timestamp, + data, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VolumePdu { + pub volume_left: u16, + pub volume_right: u16, +} + +impl VolumePdu { + const NAME: &'static str = "SNDVOL"; + + const FIXED_PART_SIZE: usize = 4; +} + +impl Encode for VolumePdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let volume = (u32::from(self.volume_right) << 16) | u32::from(self.volume_left); + dst.write_u32(volume); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for VolumePdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let volume_left = src.read_u16(); + let volume_right = src.read_u16(); + + Ok(Self { + volume_left, + volume_right, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PitchPdu { + pub pitch: u32, +} + +impl PitchPdu { + const NAME: &'static str = "SNDPITCH"; + + const FIXED_PART_SIZE: usize = 4; +} + +impl Encode for PitchPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.pitch); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> Decode<'de> for PitchPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let pitch = src.read_u32(); + + Ok(Self { pitch }) + } +} + +/// Server Audio Output Channel message (PDU prefixed with `SNDPROLOG`) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ServerAudioOutputPdu<'a> { + AudioFormat(ServerAudioFormatPdu), + CryptKey(CryptKeyPdu), + Training(TrainingPdu), + Wave(WavePdu<'a>), + WaveEncrypt(WaveEncryptPdu), + Close, + Wave2(Wave2Pdu<'a>), + Volume(VolumePdu), + Pitch(PitchPdu), +} + +impl ServerAudioOutputPdu<'_> { + const NAME: &'static str = "ServerAudioOutputPdu"; + + const FIXED_PART_SIZE: usize = 1 /* msgType */ + 1 /* padding*/ + 2 /* bodySize */; +} + +impl Encode for ServerAudioOutputPdu<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let (msg_type, pdu_size) = match self { + Self::AudioFormat(pdu) => (SNDC_FORMATS, pdu.size()), + Self::CryptKey(pdu) => (SNDC_CRYPTKEY, pdu.size()), + Self::Training(pdu) => (SNDC_TRAINING, pdu.size()), + Self::Wave(pdu) => (SNDC_WAVE, pdu.body_size()), + Self::WaveEncrypt(pdu) => (SNDC_WAVEENCRYPT, pdu.size()), + Self::Close => (SNDC_CLOSE, 0), + Self::Wave2(pdu) => (SNDC_WAVE2, pdu.size()), + Self::Volume(pdu) => (SNDC_VOLUME, pdu.size()), + Self::Pitch(pdu) => (SNDC_PITCH, pdu.size()), + }; + + dst.write_u8(msg_type); + write_padding!(dst, 1); + dst.write_u16(cast_length!("ServerAudioOutputPdu::bodySize", pdu_size)?); + + match self { + Self::AudioFormat(pdu) => pdu.encode(dst), + Self::CryptKey(pdu) => pdu.encode(dst), + Self::Training(pdu) => pdu.encode(dst), + Self::Wave(pdu) => pdu.encode(dst), + Self::WaveEncrypt(pdu) => pdu.encode(dst), + Self::Close => Ok(()), + Self::Wave2(pdu) => pdu.encode(dst), + Self::Volume(pdu) => pdu.encode(dst), + Self::Pitch(pdu) => pdu.encode(dst), + }?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(match self { + Self::AudioFormat(pdu) => pdu.size(), + Self::CryptKey(pdu) => pdu.size(), + Self::Training(pdu) => pdu.size(), + Self::Wave(pdu) => pdu.size(), + Self::WaveEncrypt(pdu) => pdu.size(), + Self::Close => 0, + Self::Wave2(pdu) => pdu.size(), + Self::Volume(pdu) => pdu.size(), + Self::Pitch(pdu) => pdu.size(), + }) + .expect("never overflow") + } +} + +impl<'de> Decode<'de> for ServerAudioOutputPdu<'_> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let msg_type = src.read_u8(); + read_padding!(src, 1); + let body_size = src.read_u16(); + + match msg_type { + SNDC_FORMATS => { + let pdu = ServerAudioFormatPdu::decode(src)?; + Ok(Self::AudioFormat(pdu)) + } + SNDC_CRYPTKEY => { + let pdu = CryptKeyPdu::decode(src)?; + Ok(Self::CryptKey(pdu)) + } + SNDC_TRAINING => { + let pdu = TrainingPdu::decode(src)?; + Ok(Self::Training(pdu)) + } + SNDC_WAVE => { + let pdu = WavePdu::decode(src, body_size)?; + Ok(Self::Wave(pdu)) + } + SNDC_WAVEENCRYPT => { + let pdu = WaveEncryptPdu::decode(src, Version::V5)?; + Ok(Self::WaveEncrypt(pdu)) + } + SNDC_CLOSE => Ok(Self::Close), + SNDC_WAVE2 => { + let pdu = Wave2Pdu::decode(src)?; + Ok(Self::Wave2(pdu)) + } + SNDC_VOLUME => { + let pdu = VolumePdu::decode(src)?; + Ok(Self::Volume(pdu)) + } + SNDC_PITCH => { + let pdu = PitchPdu::decode(src)?; + Ok(Self::Pitch(pdu)) + } + _ => Err(invalid_field_err!( + "ServerAudioOutputPdu::msgType", + "Unknown audio output PDU type" + )), + } + } +} + +impl SvcEncode for ServerAudioOutputPdu<'_> {} + +/// Client Audio Output Channel message (PDU prefixed with `SNDPROLOG`) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ClientAudioOutputPdu { + AudioFormat(ClientAudioFormatPdu), + QualityMode(QualityModePdu), + TrainingConfirm(TrainingConfirmPdu), + WaveConfirm(WaveConfirmPdu), +} + +impl ClientAudioOutputPdu { + const NAME: &'static str = "ClientAudioOutputPdu"; + + const FIXED_PART_SIZE: usize = 1 /* msgType */ + 1 /* padding*/ + 2 /* bodySize */; +} + +impl Encode for ClientAudioOutputPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + let (msg_type, body_size) = match self { + Self::AudioFormat(pdu) => (SNDC_FORMATS, pdu.size()), + Self::QualityMode(pdu) => (SNDC_QUALITYMODE, pdu.size()), + Self::TrainingConfirm(pdu) => (SNDC_TRAINING, pdu.size()), + Self::WaveConfirm(pdu) => (SNDC_WAVECONFIRM, pdu.size()), + }; + + dst.write_u8(msg_type); + write_padding!(dst, 1); + dst.write_u16(cast_length!("ClientAudioOutputPdu::bodySize", body_size)?); + + match self { + Self::AudioFormat(pdu) => pdu.encode(dst), + Self::QualityMode(pdu) => pdu.encode(dst), + Self::TrainingConfirm(pdu) => pdu.encode(dst), + Self::WaveConfirm(pdu) => pdu.encode(dst), + }?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + .checked_add(match self { + Self::AudioFormat(pdu) => pdu.size(), + Self::QualityMode(pdu) => pdu.size(), + Self::TrainingConfirm(pdu) => pdu.size(), + Self::WaveConfirm(pdu) => pdu.size(), + }) + .expect("never overflow") + } +} + +impl<'de> Decode<'de> for ClientAudioOutputPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let msg_type = src.read_u8(); + read_padding!(src, 1); + let _body_size = src.read_u16(); + + match msg_type { + SNDC_FORMATS => { + let pdu = ClientAudioFormatPdu::decode(src)?; + Ok(Self::AudioFormat(pdu)) + } + SNDC_QUALITYMODE => { + let pdu = QualityModePdu::decode(src)?; + Ok(Self::QualityMode(pdu)) + } + SNDC_TRAINING => { + let pdu = TrainingConfirmPdu::decode(src)?; + Ok(Self::TrainingConfirm(pdu)) + } + SNDC_WAVECONFIRM => { + let pdu = WaveConfirmPdu::decode(src)?; + Ok(Self::WaveConfirm(pdu)) + } + _ => Err(invalid_field_err!( + "ClientAudioOutputPdu::msgType", + "Unknown audio output PDU type" + )), + } + } +} + +impl SvcEncode for ClientAudioOutputPdu {} diff --git a/crates/ironrdp-rdpsnd/src/server.rs b/crates/ironrdp-rdpsnd/src/server.rs new file mode 100644 index 00000000..435b4c57 --- /dev/null +++ b/crates/ironrdp-rdpsnd/src/server.rs @@ -0,0 +1,236 @@ +use ironrdp_core::{impl_as_any, Decode as _, ReadCursor}; +use ironrdp_pdu::gcc::ChannelName; +use ironrdp_pdu::{decode_err, pdu_other_err, PduResult}; +use ironrdp_svc::{CompressionCondition, SvcMessage, SvcProcessor, SvcProcessorMessages, SvcServerProcessor}; +use tracing::{debug, error}; + +use crate::pdu::{self, ClientAudioFormatPdu, QualityMode}; + +pub type RdpsndSvcMessages = SvcProcessorMessages; + +pub trait RdpsndError: core::error::Error + Send + Sync + 'static {} + +impl RdpsndError for T where T: core::error::Error + Send + Sync + 'static {} + +/// Message sent by the event loop. +#[derive(Debug)] +pub enum RdpsndServerMessage { + /// Wave data, with timestamp + Wave(Vec, u32), + SetVolume { + left: u16, + right: u16, + }, + Close, + /// Failure received from the OS event loop. + /// + /// Implementation should log/display this error. + Error(Box), +} + +pub trait RdpsndServerHandler: Send + core::fmt::Debug { + fn get_formats(&self) -> &[pdu::AudioFormat]; + + fn start(&mut self, client_format: &ClientAudioFormatPdu) -> Option; + + fn stop(&mut self); +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum RdpsndState { + Start, + WaitingForClientFormats, + WaitingForQualityMode, + WaitingForTrainingConfirm, + Ready, + Stop, +} + +#[derive(Debug)] +pub struct RdpsndServer { + handler: Box, + state: RdpsndState, + client_format: Option, + quality_mode: Option, + block_no: u8, + format_no: Option, +} + +impl RdpsndServer { + pub const NAME: ChannelName = ChannelName::from_static(b"rdpsnd\0\0"); + + pub fn new(handler: Box) -> Self { + Self { + handler, + state: RdpsndState::Start, + client_format: None, + quality_mode: None, + format_no: None, + block_no: 0, + } + } + + pub fn version(&self) -> PduResult { + let client_format = self + .client_format + .as_ref() + .ok_or_else(|| pdu_other_err!("invalid state, client format not yet received"))?; + + Ok(client_format.version) + } + + pub fn flags(&self) -> PduResult { + let client_format = self + .client_format + .as_ref() + .ok_or_else(|| pdu_other_err!("invalid state, client format not yet received"))?; + + Ok(client_format.flags) + } + + pub fn training_pdu(&mut self) -> PduResult { + let pdu = pdu::TrainingPdu { + timestamp: 4231, // a random number + data: vec![], + }; + Ok(RdpsndSvcMessages::new(vec![ + pdu::ServerAudioOutputPdu::Training(pdu).into() + ])) + } + + pub fn wave(&mut self, data: Vec, ts: u32) -> PduResult { + let version = self.version()?; + let format_no = self + .format_no + .ok_or_else(|| pdu_other_err!("invalid state - no format"))?; + + // The server doesn't wait for wave confirm, apparently FreeRDP neither. + let msg = if version >= pdu::Version::V8 { + let pdu = pdu::Wave2Pdu { + block_no: self.block_no, + timestamp: 0, + audio_timestamp: ts, + format_no, + data: data.into(), + }; + RdpsndSvcMessages::new(vec![pdu::ServerAudioOutputPdu::Wave2(pdu).into()]) + } else { + let pdu = pdu::WavePdu { + block_no: self.block_no, + format_no, + timestamp: 0, + data: data.into(), + }; + RdpsndSvcMessages::new(vec![pdu::ServerAudioOutputPdu::Wave(pdu).into()]) + }; + + self.block_no = self.block_no.overflowing_add(1).0; + + Ok(msg) + } + + pub fn set_volume(&mut self, volume_left: u16, volume_right: u16) -> PduResult { + if !self.flags()?.contains(pdu::AudioFormatFlags::VOLUME) { + return Err(pdu_other_err!("client doesn't support volume")); + } + let pdu = pdu::VolumePdu { + volume_left, + volume_right, + }; + Ok(RdpsndSvcMessages::new(vec![ + pdu::ServerAudioOutputPdu::Volume(pdu).into() + ])) + } + + pub fn close(&mut self) -> PduResult { + Ok(RdpsndSvcMessages::new(vec![pdu::ServerAudioOutputPdu::Close.into()])) + } +} + +impl_as_any!(RdpsndServer); + +impl SvcProcessor for RdpsndServer { + fn channel_name(&self) -> ChannelName { + Self::NAME + } + + fn compression_condition(&self) -> CompressionCondition { + CompressionCondition::Never + } + + fn process(&mut self, payload: &[u8]) -> PduResult> { + let pdu = pdu::ClientAudioOutputPdu::decode(&mut ReadCursor::new(payload)).map_err(|e| decode_err!(e))?; + debug!(?pdu); + let msg = match self.state { + RdpsndState::WaitingForClientFormats => { + let pdu::ClientAudioOutputPdu::AudioFormat(af) = pdu else { + error!("Invalid PDU"); + self.state = RdpsndState::Stop; + return Ok(vec![]); + }; + self.client_format = Some(af); + if self.version()? >= pdu::Version::V6 { + self.state = RdpsndState::WaitingForQualityMode; + vec![] + } else { + self.state = RdpsndState::WaitingForTrainingConfirm; + self.training_pdu()?.into() + } + } + RdpsndState::WaitingForQualityMode => { + let pdu::ClientAudioOutputPdu::QualityMode(pdu) = pdu else { + error!("Invalid PDU"); + self.state = RdpsndState::Stop; + return Ok(vec![]); + }; + self.quality_mode = Some(pdu.quality_mode); + self.state = RdpsndState::WaitingForTrainingConfirm; + self.training_pdu()?.into() + } + RdpsndState::WaitingForTrainingConfirm => { + let pdu::ClientAudioOutputPdu::TrainingConfirm(_) = pdu else { + error!("Invalid PDU"); + self.state = RdpsndState::Stop; + return Ok(vec![]); + }; + let client_format = self.client_format.as_ref().expect("available in this state"); + self.state = RdpsndState::Ready; + self.format_no = self.handler.start(client_format); + vec![] + } + RdpsndState::Ready => { + if let pdu::ClientAudioOutputPdu::WaveConfirm(c) = pdu { + debug!(?c); + } + vec![] + } + state => { + error!(?state, "Invalid state"); + vec![] + } + }; + Ok(msg) + } + + fn start(&mut self) -> PduResult> { + if self.state != RdpsndState::Start { + error!("Attempted to start rdpsnd channel in invalid state"); + } + + let pdu = pdu::ServerAudioOutputPdu::AudioFormat(pdu::ServerAudioFormatPdu { + version: pdu::Version::V8, + formats: self.handler.get_formats().into(), + }); + + self.state = RdpsndState::WaitingForClientFormats; + Ok(vec![SvcMessage::from(pdu)]) + } +} + +impl Drop for RdpsndServer { + fn drop(&mut self) { + self.handler.stop(); + } +} + +impl SvcServerProcessor for RdpsndServer {} diff --git a/crates/ironrdp-replay-client/Cargo.toml b/crates/ironrdp-replay-client/Cargo.toml new file mode 100644 index 00000000..3ba54728 --- /dev/null +++ b/crates/ironrdp-replay-client/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ironrdp-replay-client" +version = "0.1.0" +readme = "README.md" +description = "Utility tool to replay RDP graphics pipeline for debugging purposes" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +ironrdp.workspace = true +ironrdp-glutin-renderer.workspace = true +tracing.workspace = true +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +clap = { version = "4.2", features = ["derive", "cargo"] } +glutin = { version = "0.29" } + +[lints] +workspace = true + diff --git a/crates/ironrdp-replay-client/README.md b/crates/ironrdp-replay-client/README.md new file mode 100644 index 00000000..8600a2ef --- /dev/null +++ b/crates/ironrdp-replay-client/README.md @@ -0,0 +1,5 @@ +Utility tool to parse data dumped through the RDP graphics pipeline and replay it. This tool is helpful to iterate and fix any issues in the rendering pipeline. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-replay-client/scripts/runtests.ps1 b/crates/ironrdp-replay-client/scripts/runtests.ps1 new file mode 100755 index 00000000..cc4503a0 --- /dev/null +++ b/crates/ironrdp-replay-client/scripts/runtests.ps1 @@ -0,0 +1,4 @@ +cargo run --bin ironrdp-replay-client -- --close --frame-rate=30 --data-file $PSScriptRoot/../test_data/sample1_avc444.data && ` +cargo run --bin ironrdp-replay-client -- --close --frame-rate=30 --data-file $PSScriptRoot/../test_data/sample2_avc444.data && ` +cargo run --bin ironrdp-replay-client -- --close --frame-rate=30 --data-file $PSScriptRoot/../test_data/sample1_avc444v2.data && ` +cargo run --bin ironrdp-replay-client -- --close --frame-rate=30 --data-file $PSScriptRoot/../test_data/sample2_avc444v2.data \ No newline at end of file diff --git a/crates/ironrdp-replay-client/src/main.rs b/crates/ironrdp-replay-client/src/main.rs new file mode 100644 index 00000000..90af089c --- /dev/null +++ b/crates/ironrdp-replay-client/src/main.rs @@ -0,0 +1,121 @@ +use std::fs::File; +use std::io::ErrorKind; +use std::path::PathBuf; +use std::process::exit; +use std::sync::mpsc::sync_channel; +use std::thread; +use std::time::Duration; + +use clap::Parser; +use glutin::dpi::PhysicalSize; +use glutin::event::{Event, WindowEvent}; +use glutin::event_loop::ControlFlow; +use ironrdp::pdu::dvc::gfx::{GraphicsPipelineError, ServerPdu}; +use ironrdp_glutin_renderer::renderer::Renderer; + +pub type Error = Box; + +/// Devolutions IronRDP client +#[derive(Parser, Debug)] +#[clap(version, long_about = None)] +struct Args { + /// A file to use for the data file + #[clap(long, value_parser)] + data_file: PathBuf, + + ////// Frame rate + #[clap(long, value_parser, default_value_t = 1)] + frame_rate: u32, + + // Close on completion + #[clap(long, value_parser)] + close: bool, +} + +pub enum UserEvent {} + +fn create_ui_context() -> ( + glutin::ContextWrapper, + glutin::event_loop::EventLoop, +) { + let event_loop = glutin::event_loop::EventLoopBuilder::with_user_event().build(); + let window_builder = glutin::window::WindowBuilder::new() + .with_title("RDP Replay Helper!") + .with_resizable(false) + .with_inner_size(PhysicalSize { width: 0, height: 0 }); + let window = glutin::ContextBuilder::new() + .with_vsync(true) + .build_windowed(window_builder, &event_loop) + .unwrap(); + (window, event_loop) +} + +pub fn main() -> Result<(), Error> { + tracing_subscriber::fmt().compact().init(); + + let args = Args::parse(); + + let (sender, receiver) = sync_channel(1); + + let (window, event_loop) = create_ui_context(); + let renderer = Renderer::new(window, receiver, None); + + thread::spawn(move || { + let result = handle_file(sender, args); + info!("Result: {:?}", result); + }); + + event_loop.run(move |main_event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match &main_event { + Event::LoopDestroyed => {} + Event::RedrawRequested(_) => { + let res = renderer.repaint(); + if res.is_err() { + error!("Repaint send error: {:?}", res); + } + } + Event::WindowEvent { ref event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::Resized(size) => { + info!("Window resized {:?}", size); + } + _ => {} + }, + _ => (), + } + }); +} + +// Parse the graphics file and send it to renderer 1 event at a time +fn handle_file(sender: std::sync::mpsc::SyncSender, args: Args) -> Result<(), Error> { + let file = File::open(args.data_file).unwrap(); + let delay = 1000 / args.frame_rate as u64; + + loop { + let packet = ServerPdu::from_buffer(&file); + if let Ok(packet) = packet { + let wait = matches!(packet, ServerPdu::WireToSurface1(..)); + sender.send(packet)?; + if wait { + thread::sleep(Duration::from_millis(delay)); + } + } else { + let ignorable = if let Err(GraphicsPipelineError::IOError(e)) = packet.as_ref() { + e.kind() == ErrorKind::UnexpectedEof + } else { + false + }; + + if !ignorable { + error!("Error: {:?}", packet); + } + + if args.close { + exit(0); + } + return Err(Error::from("S".to_string())); + } + } +} diff --git a/crates/ironrdp-replay-client/test_data/README.md b/crates/ironrdp-replay-client/test_data/README.md new file mode 100644 index 00000000..13bb571c --- /dev/null +++ b/crates/ironrdp-replay-client/test_data/README.md @@ -0,0 +1,3 @@ +Data generated using a custom parser and data file from [Here](https://github.com/microsoft/WindowsProtocolTestSuites/tree/main/TestSuites/RDP/Client/src/TestSuite/RDPEGFX/H264TestData) + +Some data generated using custom code in FreeRDP GFX pipeline to dump stream to file. \ No newline at end of file diff --git a/crates/ironrdp-replay-client/test_data/sample1_avc444.data b/crates/ironrdp-replay-client/test_data/sample1_avc444.data new file mode 100644 index 00000000..cb8d5dbb Binary files /dev/null and b/crates/ironrdp-replay-client/test_data/sample1_avc444.data differ diff --git a/crates/ironrdp-replay-client/test_data/sample1_avc444v2.data b/crates/ironrdp-replay-client/test_data/sample1_avc444v2.data new file mode 100644 index 00000000..bc696a6d Binary files /dev/null and b/crates/ironrdp-replay-client/test_data/sample1_avc444v2.data differ diff --git a/crates/ironrdp-replay-client/test_data/sample2_avc444.data b/crates/ironrdp-replay-client/test_data/sample2_avc444.data new file mode 100644 index 00000000..7c3977fa Binary files /dev/null and b/crates/ironrdp-replay-client/test_data/sample2_avc444.data differ diff --git a/crates/ironrdp-replay-client/test_data/sample2_avc444v2.data b/crates/ironrdp-replay-client/test_data/sample2_avc444v2.data new file mode 100644 index 00000000..d8878900 Binary files /dev/null and b/crates/ironrdp-replay-client/test_data/sample2_avc444v2.data differ diff --git a/crates/ironrdp-server/CHANGELOG.md b/crates/ironrdp-server/CHANGELOG.md new file mode 100644 index 00000000..916740d6 --- /dev/null +++ b/crates/ironrdp-server/CHANGELOG.md @@ -0,0 +1,174 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.9.0...ironrdp-server-v0.10.0)] - 2025-12-18 + +### Bug Fixes + +- Send TLS close_notify during graceful RDP disconnect ([#1032](https://github.com/Devolutions/IronRDP/issues/1032)) ([a70e01d9c5](https://github.com/Devolutions/IronRDP/commit/a70e01d9c5675a7dffd65eda7428537c8ad6a857)) + + Add support for sending a proper TLS close_notify message when the RDP + client initiates a graceful disconnect PDU. + +## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.8.0...ironrdp-server-v0.9.0)] - 2025-09-24 + +### Bug Fixes + +- [**breaking**] RdpServerDisplayUpdates::next_update now returns a Result + +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.7.0...ironrdp-server-v0.8.0)] - 2025-08-29 + +### Features + +- [**breaking**] Add server_codecs_capabilities() ([d3aaa43c23](https://github.com/Devolutions/IronRDP/commit/d3aaa43c23b252077b8720bb8ecfeceaaf7b7a7f)) + + Teach the server to support customizable codecs set. Use the same + logic/parsing as the client codecs configuration. + + Replace "with_remote_fx" with "codecs". + +- Add QOI image codec ([613fd51f26](https://github.com/Devolutions/IronRDP/commit/613fd51f26315d8212662c46f8e625c541e4bb59)) + + The Quite OK Image format ([1]) losslessly compresses images to a similar size + of PNG, while offering 20x-50x faster encoding and 3x-4x faster decoding. + +- Add QOIZ image codec ([87df67fdc7](https://github.com/Devolutions/IronRDP/commit/87df67fdc76ff4f39d4b83521e34bf3b5e2e73bb)) + + Add a new QOIZ codec for SetSurface command. The PDU data contains the same + data as the QOI codec, with zstd compression. + +## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.6.1...ironrdp-server-v0.7.0)] - 2025-07-08 + +### Build + +- Update sspi dependency (#839) ([33530212c4](https://github.com/Devolutions/IronRDP/commit/33530212c42bf28c875ac078ed2408657831b417)) + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.5.0...ironrdp-server-v0.6.0)] - 2025-05-27 + +### Features + +- Add stride debug info ([7f57817805](https://github.com/Devolutions/IronRDP/commit/7f578178056282e590179a10cd1eedb8f4d9ad63)) + +- Add Framebuffer helper struct ([1e87961d16](https://github.com/Devolutions/IronRDP/commit/1e87961d1611ed31f58b407f208295c97c0d2944)) + + This will hold the updated bitmap data for the whole framebuffer. + +- Add BitmapUpdate::sub() ([a76e84d459](https://github.com/Devolutions/IronRDP/commit/a76e84d45927d61e21c27abcfa31c4f0c7a17bbf)) + +- Implement some Encoder Debug ([137d91ae7a](https://github.com/Devolutions/IronRDP/commit/137d91ae7a096170ada289d420785c8f5de0663b)) + +- Keep last full-frame/desktop update ([aeb1193674](https://github.com/Devolutions/IronRDP/commit/aeb1193674641846ae1873def8c84a62a59213d5)) + + It should reflect client drawing state. + + In following changes, we will fix it to draw bitmap updates on it, to + keep it up to date. + +- Find and send the damaged tiles ([fb3769c4a7](https://github.com/Devolutions/IronRDP/commit/fb3769c4a7fce56e340df8c4b19f7d90cda93e50)) + + Keep a framebuffer and tile-diff against it, to save from + encoding/sending the same bitmap data regions. + +### Bug Fixes + +- Use desktop size for RFX channel size (#756) ([806f1d7694](https://github.com/Devolutions/IronRDP/commit/806f1d7694313b1a59842af300a437ae2f6c2463)) + +- [**breaking**] Remove time_warn! from the public API (#773) ([cc78b1e3dc](https://github.com/Devolutions/IronRDP/commit/cc78b1e3dc1c554dd3fcf6494763caa00ba28ad7)) + + This is intended to be an internal macro. + +### Refactor + +- [**breaking**] Drop support for pixelOrder ([db6f4cdb7f](https://github.com/Devolutions/IronRDP/commit/db6f4cdb7f379713979b930e8e1fa1a813ebecc4)) + + Dealing with multiple formats is sufficiently annoying, there isn't much + need for awkward image layout. This was done for efficiency reason for + bitmap encoding, but bitmap is really inefficient anyway and very few + servers will actually provide bottom to top images (except with GL/GPU + textures, but this is not in scope yet). + +- [**breaking**] Use bytes, allowing shareable bitmap data ([3c43fdda76](https://github.com/Devolutions/IronRDP/commit/3c43fdda76f4ef6413db4010471364d6b1be2798)) + +- [**breaking**] Rename left/top -> x/y ([229070a435](https://github.com/Devolutions/IronRDP/commit/229070a43554927a01541052a819fe3fcd32a913)) + + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.4.2...ironrdp-server-v0.5.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + + +## [[0.4.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.4.1...ironrdp-server-v0.4.2)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + + +## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.4.0...ironrdp-server-v0.4.1)] - 2025-01-28 + +### Features + +- Advertize Bitmap::desktopResizeFlag ([a0fccf8d1a](https://github.com/Devolutions/IronRDP/commit/a0fccf8d1a3eeab6c73ed7d9cdbb4342cca173c4)) + + This makes freerdp keep the flag up and handle desktop + resize/deactivation-reactivation. It should be okay to advertize, + if the server doesn't resize anyway, I guess. + +- Add volume support (#641) ([a6c36511f6](https://github.com/Devolutions/IronRDP/commit/a6c36511f6584f67b8c6e795c34d5007ec2b24a4)) + + Add server messages and API to support setting client volume. + +### Bug Fixes + +- Drop unexpected PDUs during deactivation-reactivation ([63963182b5](https://github.com/Devolutions/IronRDP/commit/63963182b5af6ad45dc638e93de4b8a0b565c7d3)) + + The current behaviour of handling unmatched PDUs in fn read_by_hint() + isn't good enough. An unexpected PDUs may be received and fail to be + decoded during Acceptor::step(). + + Change the code to simply drop unexpected PDUs (as opposed to attempting + to replay the unmatched leftover, which isn't clearly needed) + +- Reattach existing channels ([c4587b537c](https://github.com/Devolutions/IronRDP/commit/c4587b537c7c0a148e11bc365bc3df88e2c92312)) + + I couldn't find any explicit behaviour described in the specification, + but apparently, we must just keep the channel state as they were during + reactivation. This fixes various state issues during client resize. + +- Do not restart static channels on reactivation ([82c7c2f5b0](https://github.com/Devolutions/IronRDP/commit/82c7c2f5b08c44b1a4f6b04c13ad24d9e2ffa371)) + +- Check client size ([0f9877ad39](https://github.com/Devolutions/IronRDP/commit/0f9877ad3901b37f58406095e05f345fbc8a5eaa)) + + It's problematic when the client didn't resize, as we send bitmap + updates that don't fit. The client will likely drop the connection. + Let's have a warning for this case in the server. + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.3.1...ironrdp-server-v0.4.0)] - 2024-12-17 + +### Features + +- [**breaking**] Make TlsIdentityCtx accept PEM files ([#623](https://github.com/Devolutions/IronRDP/pull/623)) ([9198284263](https://github.com/Devolutions/IronRDP/commit/9198284263e11706fed76310f796200b75111126)) + + This is in general more convenient than DER files. + + This patch also includes a breaking change in the public API. + The `cert` field in the `TlsIdentityCtx` struct is replaced by a `certs` field containing multiple `CertificateDer` items. + +## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.3.0...ironrdp-server-v0.3.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-server/Cargo.toml b/crates/ironrdp-server/Cargo.toml new file mode 100644 index 00000000..b1e4650c --- /dev/null +++ b/crates/ironrdp-server/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "ironrdp-server" +version = "0.10.0" +readme = "README.md" +description = "Extendable skeleton for implementing custom RDP servers" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = true +test = false + +[features] +default = ["rayon", "qoi", "qoiz"] +helper = ["dep:x509-cert", "dep:rustls-pemfile"] +rayon = ["dep:rayon"] +qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"] +qoiz = ["dep:zstd-safe", "qoi", "ironrdp-pdu/qoiz"] + +# Internal (PRIVATE!) features used to aid testing. +# Don't rely on these whatsoever. They may disappear at any time. +__bench = ["dep:visibility"] + +[dependencies] +anyhow = "1.0" +tokio = { version = "1", features = ["net", "macros", "sync", "rt"] } # public +tokio-rustls = "0.26" # public +async-trait = "0.1" +ironrdp-async = { path = "../ironrdp-async", version = "0.8" } +ironrdp-ainput = { path = "../ironrdp-ainput", version = "0.4" } +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.5" } # public +ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.4" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.4" } # public +ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.8" } +ironrdp-acceptor = { path = "../ironrdp-acceptor", version = "0.8" } # public +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.7" } # public +ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.6" } # public +tracing = { version = "0.1", features = ["log"] } +x509-cert = { version = "0.2.5", optional = true } +rustls-pemfile = { version = "2.2.0", optional = true } +rayon = { version = "1.10.0", optional = true } +bytes = "1" +visibility = { version = "0.1", optional = true } +qoicoubeh = { version = "0.5", optional = true } +zstd-safe = { version = "7.2", optional = true } + +[dev-dependencies] +tokio = { version = "1", features = ["sync"] } + +[lints] +workspace = true diff --git a/crates/ironrdp-server/LICENSE-APACHE b/crates/ironrdp-server/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-server/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-server/LICENSE-MIT b/crates/ironrdp-server/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-server/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-server/README.md b/crates/ironrdp-server/README.md new file mode 100644 index 00000000..82a817b1 --- /dev/null +++ b/crates/ironrdp-server/README.md @@ -0,0 +1,29 @@ +# IronRDP Server + +Extendable skeleton for implementing custom RDP servers. + +For now, it requires the [Tokio runtime](https://tokio.rs/). + +--- + +The server currently supports: + +**Security** + - Enhanced RDP Security with TLS External Security Protocols (TLS 1.2 and TLS 1.3) + +**Input** + - FastPath input events + - x224 input events and disconnect + +**Codecs** + - bitmap display updates with RDP 6.0 compression + +--- + +Custom logic for your RDP server can be added by implementing these traits: + - `RdpServerInputHandler` - callbacks used when the server receives input events from a client + - `RdpServerDisplay` - notifies the server of display updates + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-server/src/builder.rs b/crates/ironrdp-server/src/builder.rs new file mode 100644 index 00000000..499e00b3 --- /dev/null +++ b/crates/ironrdp-server/src/builder.rs @@ -0,0 +1,205 @@ +use core::net::SocketAddr; + +use anyhow::Result; +use ironrdp_pdu::rdp::capability_sets::{server_codecs_capabilities, BitmapCodecs}; +use tokio_rustls::TlsAcceptor; + +use super::clipboard::CliprdrServerFactory; +use super::display::{DesktopSize, RdpServerDisplay}; +use super::handler::{KeyboardEvent, MouseEvent, RdpServerInputHandler}; +use super::server::{RdpServer, RdpServerOptions, RdpServerSecurity}; +use crate::{DisplayUpdate, RdpServerDisplayUpdates, SoundServerFactory}; + +pub struct WantsAddr {} +pub struct WantsSecurity { + addr: SocketAddr, +} +pub struct WantsHandler { + addr: SocketAddr, + security: RdpServerSecurity, +} +pub struct WantsDisplay { + addr: SocketAddr, + security: RdpServerSecurity, + handler: Box, +} +pub struct BuilderDone { + addr: SocketAddr, + security: RdpServerSecurity, + codecs: BitmapCodecs, + handler: Box, + display: Box, + cliprdr_factory: Option>, + sound_factory: Option>, +} + +pub struct RdpServerBuilder { + state: State, +} + +impl RdpServerBuilder { + pub fn new() -> Self { + Self { state: WantsAddr {} } + } + + #[expect(clippy::unused_self)] // ensuring state transition from WantsAddr + pub fn with_addr(self, addr: impl Into) -> RdpServerBuilder { + RdpServerBuilder { + state: WantsSecurity { addr: addr.into() }, + } + } +} + +impl Default for RdpServerBuilder { + fn default() -> Self { + Self::new() + } +} + +impl RdpServerBuilder { + pub fn with_no_security(self) -> RdpServerBuilder { + RdpServerBuilder { + state: WantsHandler { + addr: self.state.addr, + security: RdpServerSecurity::None, + }, + } + } + + pub fn with_tls(self, acceptor: impl Into) -> RdpServerBuilder { + RdpServerBuilder { + state: WantsHandler { + addr: self.state.addr, + security: RdpServerSecurity::Tls(acceptor.into()), + }, + } + } + + pub fn with_hybrid(self, acceptor: impl Into, pub_key: Vec) -> RdpServerBuilder { + RdpServerBuilder { + state: WantsHandler { + addr: self.state.addr, + security: RdpServerSecurity::Hybrid((acceptor.into(), pub_key)), + }, + } + } +} + +impl RdpServerBuilder { + pub fn with_input_handler(self, handler: H) -> RdpServerBuilder + where + H: RdpServerInputHandler + 'static, + { + RdpServerBuilder { + state: WantsDisplay { + addr: self.state.addr, + security: self.state.security, + handler: Box::new(handler), + }, + } + } + + pub fn with_no_input(self) -> RdpServerBuilder { + RdpServerBuilder { + state: WantsDisplay { + addr: self.state.addr, + security: self.state.security, + handler: Box::new(NoopInputHandler), + }, + } + } +} + +impl RdpServerBuilder { + pub fn with_display_handler(self, display: D) -> RdpServerBuilder + where + D: RdpServerDisplay + 'static, + { + RdpServerBuilder { + state: BuilderDone { + addr: self.state.addr, + security: self.state.security, + handler: self.state.handler, + display: Box::new(display), + sound_factory: None, + cliprdr_factory: None, + codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"), + }, + } + } + + pub fn with_no_display(self) -> RdpServerBuilder { + RdpServerBuilder { + state: BuilderDone { + addr: self.state.addr, + security: self.state.security, + handler: self.state.handler, + display: Box::new(NoopDisplay), + sound_factory: None, + cliprdr_factory: None, + codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"), + }, + } + } +} + +impl RdpServerBuilder { + pub fn with_cliprdr_factory(mut self, cliprdr_factory: Option>) -> Self { + self.state.cliprdr_factory = cliprdr_factory; + self + } + + pub fn with_sound_factory(mut self, sound: Option>) -> Self { + self.state.sound_factory = sound; + self + } + + pub fn with_bitmap_codecs(mut self, codecs: BitmapCodecs) -> Self { + self.state.codecs = codecs; + self + } + + pub fn build(self) -> RdpServer { + RdpServer::new( + RdpServerOptions { + addr: self.state.addr, + security: self.state.security, + codecs: self.state.codecs, + }, + self.state.handler, + self.state.display, + self.state.sound_factory, + self.state.cliprdr_factory, + ) + } +} + +struct NoopInputHandler; + +impl RdpServerInputHandler for NoopInputHandler { + fn keyboard(&mut self, _: KeyboardEvent) {} + fn mouse(&mut self, _: MouseEvent) {} +} + +struct NoopDisplayUpdates; + +#[async_trait::async_trait] +impl RdpServerDisplayUpdates for NoopDisplayUpdates { + async fn next_update(&mut self) -> Result> { + let () = core::future::pending().await; + unreachable!() + } +} + +struct NoopDisplay; + +#[async_trait::async_trait] +impl RdpServerDisplay for NoopDisplay { + async fn size(&mut self) -> DesktopSize { + DesktopSize { width: 0, height: 0 } + } + + async fn updates(&mut self) -> Result> { + Ok(Box::new(NoopDisplayUpdates {})) + } +} diff --git a/crates/ironrdp-server/src/capabilities.rs b/crates/ironrdp-server/src/capabilities.rs new file mode 100644 index 00000000..5a7cc8ea --- /dev/null +++ b/crates/ironrdp-server/src/capabilities.rs @@ -0,0 +1,89 @@ +use ironrdp_pdu::rdp::capability_sets::{self, GeneralExtraFlags}; + +use crate::{DesktopSize, RdpServerOptions}; + +pub(crate) fn capabilities(opts: &RdpServerOptions, size: DesktopSize) -> Vec { + vec![ + capability_sets::CapabilitySet::General(general_capabilities()), + capability_sets::CapabilitySet::Bitmap(bitmap_capabilities(&size)), + capability_sets::CapabilitySet::Order(order_capabilities()), + capability_sets::CapabilitySet::SurfaceCommands(surface_capabilities()), + capability_sets::CapabilitySet::Pointer(pointer_capabilities()), + capability_sets::CapabilitySet::Input(input_capabilities()), + capability_sets::CapabilitySet::VirtualChannel(virtual_channel_capabilities()), + capability_sets::CapabilitySet::MultiFragmentUpdate(multifragment_update()), + capability_sets::CapabilitySet::BitmapCodecs(opts.codecs.clone()), + ] +} + +fn general_capabilities() -> capability_sets::General { + capability_sets::General { + extra_flags: GeneralExtraFlags::FASTPATH_OUTPUT_SUPPORTED, + ..Default::default() + } +} + +fn bitmap_capabilities(size: &DesktopSize) -> capability_sets::Bitmap { + capability_sets::Bitmap { + pref_bits_per_pix: 32, + desktop_width: size.width, + desktop_height: size.height, + // This makes freerdp keep the flag up and handle desktop resize/deactivation-reactivation. + // Likely okay to advertize if the server doesn't resize anyway. + desktop_resize_flag: true, + drawing_flags: capability_sets::BitmapDrawingFlags::empty(), + } +} + +fn order_capabilities() -> capability_sets::Order { + capability_sets::Order::new( + capability_sets::OrderFlags::empty(), + capability_sets::OrderSupportExFlags::empty(), + 2048, + 224, + ) +} + +fn surface_capabilities() -> capability_sets::SurfaceCommands { + capability_sets::SurfaceCommands { + flags: capability_sets::CmdFlags::all(), + } +} + +fn pointer_capabilities() -> capability_sets::Pointer { + capability_sets::Pointer { + color_pointer_cache_size: 2048, + pointer_cache_size: 2048, + } +} + +fn input_capabilities() -> capability_sets::Input { + capability_sets::Input { + input_flags: capability_sets::InputFlags::SCANCODES + | capability_sets::InputFlags::MOUSE_RELATIVE + | capability_sets::InputFlags::MOUSEX + | capability_sets::InputFlags::FASTPATH_INPUT + | capability_sets::InputFlags::UNICODE + | capability_sets::InputFlags::FASTPATH_INPUT_2, + keyboard_layout: 0, + keyboard_type: None, + keyboard_subtype: 0, + keyboard_function_key: 128, + keyboard_ime_filename: "".into(), + } +} + +fn virtual_channel_capabilities() -> capability_sets::VirtualChannel { + capability_sets::VirtualChannel { + flags: capability_sets::VirtualChannelFlags::NO_COMPRESSION, + chunk_size: None, + } +} + +fn multifragment_update() -> capability_sets::MultifragmentUpdate { + capability_sets::MultifragmentUpdate { + // FIXME(#318): use an acceptable value for msctc. + // What is the actual server max size? + max_request_size: 16_777_215, + } +} diff --git a/crates/ironrdp-server/src/clipboard.rs b/crates/ironrdp-server/src/clipboard.rs new file mode 100644 index 00000000..3f660052 --- /dev/null +++ b/crates/ironrdp-server/src/clipboard.rs @@ -0,0 +1,5 @@ +use ironrdp_cliprdr::backend::CliprdrBackendFactory; + +use crate::ServerEventSender; + +pub trait CliprdrServerFactory: CliprdrBackendFactory + ServerEventSender {} diff --git a/crates/ironrdp-server/src/display.rs b/crates/ironrdp-server/src/display.rs new file mode 100644 index 00000000..2450b201 --- /dev/null +++ b/crates/ironrdp-server/src/display.rs @@ -0,0 +1,341 @@ +use core::num::{NonZeroU16, NonZeroUsize}; + +use anyhow::Result; +use bytes::{Bytes, BytesMut}; +use ironrdp_displaycontrol::pdu::DisplayControlMonitorLayout; +use ironrdp_graphics::diff; +use ironrdp_pdu::pointer::PointerPositionAttribute; +use tracing::{debug, warn}; + +#[rustfmt::skip] +pub use ironrdp_acceptor::DesktopSize; +pub use ironrdp_graphics::image_processing::PixelFormat; + +/// Display Update +/// +/// Contains all types of display updates currently supported by the server implementation +/// and the RDP spec +/// +#[derive(Debug, Clone)] +pub enum DisplayUpdate { + Resize(DesktopSize), + Bitmap(BitmapUpdate), + PointerPosition(PointerPositionAttribute), + ColorPointer(ColorPointer), + RGBAPointer(RGBAPointer), + HidePointer, + DefaultPointer, +} + +#[derive(Clone)] +pub struct RGBAPointer { + pub width: u16, + pub height: u16, + pub hot_x: u16, + pub hot_y: u16, + pub data: Vec, +} + +impl core::fmt::Debug for RGBAPointer { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("RGBAPointer") + .field("with", &self.width) + .field("height", &self.height) + .field("hot_x", &self.hot_x) + .field("hot_y", &self.hot_y) + .field("data_len", &self.data.len()) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct ColorPointer { + pub width: u16, + pub height: u16, + pub hot_x: u16, + pub hot_y: u16, + pub and_mask: Vec, + pub xor_mask: Vec, +} + +pub struct Framebuffer { + pub width: NonZeroU16, + pub height: NonZeroU16, + pub format: PixelFormat, + pub data: BytesMut, + pub stride: usize, +} + +impl TryInto for BitmapUpdate { + type Error = &'static str; + + fn try_into(self) -> Result { + assert_eq!(self.x, 0); + assert_eq!(self.y, 0); + Ok(Framebuffer { + width: self.width, + height: self.height, + format: self.format, + data: self.data.into(), + stride: self.stride.get(), + }) + } +} + +impl Framebuffer { + pub fn new(width: NonZeroU16, height: NonZeroU16, format: PixelFormat) -> Self { + let mut data = BytesMut::new(); + let w = NonZeroUsize::from(width).get(); + let h = NonZeroUsize::from(height).get(); + let bpp = usize::from(format.bytes_per_pixel()); + data.resize(bpp * w * h, 0); + + Self { + width, + height, + format, + data, + stride: bpp * w, + } + } + + pub fn update(&mut self, bitmap: &BitmapUpdate) { + if self.format != bitmap.format { + warn!("Bitmap format mismatch, unsupported"); + return; + } + let bpp = usize::from(self.format.bytes_per_pixel()); + let x = usize::from(bitmap.x); + let y = usize::from(bitmap.y); + let width = NonZeroUsize::from(bitmap.width).get(); + let height = NonZeroUsize::from(bitmap.height).get(); + + let data = &mut self.data; + let start = y * self.stride + x * bpp; + let end = start + (height - 1) * self.stride + width * bpp; + let dst = &mut data[start..end]; + + for y in 0..height { + let start = y * bitmap.stride.get(); + let end = start + width * bpp; + + let src = bitmap.data.slice(start..end); + + let start = y * self.stride; + let end = start + width * bpp; + let dst = &mut dst[start..end]; + + dst.copy_from_slice(&src); + } + } + + pub(crate) fn update_diffs(&mut self, bitmap: &BitmapUpdate, diffs: &[diff::Rect]) { + diffs + .iter() + .filter_map(|diff| { + let x = u16::try_from(diff.x).ok()?; + let y = u16::try_from(diff.y).ok()?; + let width = u16::try_from(diff.width).ok().and_then(NonZeroU16::new)?; + let height = u16::try_from(diff.height).ok().and_then(NonZeroU16::new)?; + + bitmap.sub(x, y, width, height) + }) + .for_each(|sub| self.update(&sub)); + } +} + +/// Bitmap Display Update +/// +/// Bitmap updates are encoded using RDP 6.0 compression, fragmented and sent using +/// Fastpath Server Updates +/// +#[derive(Clone)] +pub struct BitmapUpdate { + pub x: u16, + pub y: u16, + pub width: NonZeroU16, + pub height: NonZeroU16, + pub format: PixelFormat, + pub data: Bytes, + pub stride: NonZeroUsize, +} + +impl BitmapUpdate { + /// Extracts a sub-region of the bitmap update. + /// + /// # Parameters + /// + /// - `x`: The x-coordinate of the top-left corner of the sub-region. + /// - `y`: The y-coordinate of the top-left corner of the sub-region. + /// - `width`: The width of the sub-region. + /// - `height`: The height of the sub-region. + /// + /// # Returns + /// + /// An `Option` containing a new `BitmapUpdate` representing the sub-region if the specified + /// dimensions are within the bounds of the original bitmap update, otherwise `None`. + /// + /// # Example + /// + /// ``` + /// # use core::num::NonZeroU16; + /// # use std::num::NonZeroUsize; + /// # use bytes::Bytes; + /// # use ironrdp_graphics::image_processing::PixelFormat; + /// # use ironrdp_server::BitmapUpdate; + /// let original = BitmapUpdate { + /// x: 0, + /// y: 0, + /// width: NonZeroU16::new(100).unwrap(), + /// height: NonZeroU16::new(100).unwrap(), + /// format: PixelFormat::ARgb32, + /// data: Bytes::from(vec![0; 40000]), + /// stride: NonZeroUsize::new(400).unwrap(), + /// }; + /// + /// let sub_region = original.sub(10, 10, NonZeroU16::new(50).unwrap(), NonZeroU16::new(50).unwrap()); + /// assert!(sub_region.is_some()); + /// ``` + #[must_use] + pub fn sub(&self, x: u16, y: u16, width: NonZeroU16, height: NonZeroU16) -> Option { + if x + width.get() > self.width.get() || y + height.get() > self.height.get() { + None + } else { + let bpp = usize::from(self.format.bytes_per_pixel()); + let start = usize::from(y) * self.stride.get() + usize::from(x) * bpp; + let end = start + usize::from(height.get() - 1) * self.stride.get() + usize::from(width.get()) * bpp; + Some(Self { + x: self.x + x, + y: self.y + y, + width, + height, + format: self.format, + data: self.data.slice(start..end), + stride: self.stride, + }) + } + } +} + +impl core::fmt::Debug for BitmapUpdate { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("BitmapUpdate") + .field("x", &self.x) + .field("y", &self.y) + .field("width", &self.width) + .field("height", &self.height) + .field("format", &self.format) + .field("stride", &self.stride) + .finish() + } +} + +/// Display Updates receiver for an RDP server +/// +/// The RDP server will repeatedly call the `next_update` method to receive +/// display updates which will then be encoded and sent to the client +/// +/// See [`RdpServerDisplay`] example. +#[async_trait::async_trait] +pub trait RdpServerDisplayUpdates { + /// # Cancel safety + /// + /// This method MUST be cancellation safe because it is used in a + /// `tokio::select!` statement. If some other branch completes first, it + /// MUST be guaranteed that no data is lost. + async fn next_update(&mut self) -> Result>; +} + +/// Display for an RDP server +/// +/// # Example +/// +/// ``` +///# use anyhow::Result; +/// use ironrdp_server::{DesktopSize, DisplayUpdate, RdpServerDisplay, RdpServerDisplayUpdates}; +/// +/// pub struct DisplayUpdates { +/// receiver: tokio::sync::mpsc::Receiver, +/// } +/// +/// #[async_trait::async_trait] +/// impl RdpServerDisplayUpdates for DisplayUpdates { +/// async fn next_update(&mut self) -> anyhow::Result> { +/// Ok(self.receiver.recv().await) +/// } +/// } +/// +/// pub struct DisplayHandler { +/// width: u16, +/// height: u16, +/// } +/// +/// #[async_trait::async_trait] +/// impl RdpServerDisplay for DisplayHandler { +/// async fn size(&mut self) -> DesktopSize { +/// DesktopSize { width: self.width, height: self.height } +/// } +/// +/// async fn updates(&mut self) -> Result> { +/// Ok(Box::new(DisplayUpdates { receiver: todo!() })) +/// } +/// } +/// ``` +#[async_trait::async_trait] +pub trait RdpServerDisplay: Send { + /// This method should return the current size of the display. + /// Currently, there is no way for the client to negotiate resolution, + /// so the size returned by this method will be enforced. + async fn size(&mut self) -> DesktopSize; + + /// Return a display updates receiver + async fn updates(&mut self) -> Result>; + + /// Request a new size for the display + fn request_layout(&mut self, layout: DisplayControlMonitorLayout) { + debug!(?layout, "Requesting layout") + } +} + +#[cfg(test)] +mod tests { + use core::num::{NonZeroU16, NonZeroUsize}; + + use ironrdp_graphics::diff::Rect; + use ironrdp_graphics::image_processing::PixelFormat; + + use super::{BitmapUpdate, Framebuffer}; + + #[test] + fn framebuffer_update() { + let width = NonZeroU16::new(800).unwrap(); + let height = NonZeroU16::new(600).unwrap(); + let fmt = PixelFormat::ABgr32; + let bpp = usize::from(fmt.bytes_per_pixel()); + let mut fb = Framebuffer::new(width, height, fmt); + + let width = 15; + let stride = NonZeroUsize::new(width * bpp).unwrap(); + let height = 20; + let data = vec![1u8; height * stride.get()]; + let update = BitmapUpdate { + x: 1, + y: 2, + width: NonZeroU16::new(15).unwrap(), + height: NonZeroU16::new(20).unwrap(), + format: fmt, + data: data.into(), + stride, + }; + let diffs = vec![Rect::new(2, 3, 4, 5)]; + fb.update_diffs(&update, &diffs); + let data = fb.data; + for y in 5..10 { + for x in 3..7 { + for b in 0..bpp { + assert_eq!(data[y * fb.stride + x * bpp + b], 1); + } + } + } + } +} diff --git a/crates/ironrdp-server/src/encoder/bitmap.rs b/crates/ironrdp-server/src/encoder/bitmap.rs new file mode 100644 index 00000000..8810e433 --- /dev/null +++ b/crates/ironrdp-server/src/encoder/bitmap.rs @@ -0,0 +1,117 @@ +use core::num::NonZeroUsize; + +use ironrdp_core::{cast_int, cast_length, invalid_field_err, Encode as _, WriteCursor}; +use ironrdp_graphics::image_processing::PixelFormat; +use ironrdp_graphics::rdp6::{ + ABgrChannels, ARgbChannels, BgrAChannels, BitmapEncodeError, BitmapStreamEncoder, RgbAChannels, +}; +use ironrdp_pdu::bitmap::{self, BitmapData, BitmapUpdateData, Compression}; +use ironrdp_pdu::geometry::InclusiveRectangle; + +use crate::BitmapUpdate; + +// PERF: we could also remove the need for this buffer +#[derive(Clone)] +pub(crate) struct BitmapEncoder { + buffer: Vec, +} + +impl BitmapEncoder { + pub(crate) fn new() -> Self { + Self { + buffer: vec![0; usize::from(u16::MAX)], + } + } + + pub(crate) fn encode(&mut self, bitmap: &BitmapUpdate, output: &mut [u8]) -> Result { + // FIXME: support non-multiple of 4 widths. + // + // It’s not clear how to achieve that yet, but generally, server uses multiple of 4-widths, + // and client has surface capabilities, so this path is unlikely. + if bitmap.width.get() % 4 != 0 { + return Err(BitmapEncodeError::Encode(invalid_field_err!( + "bitmap", + "Width must be a multiple of 4" + ))); + } + + let bytes_per_pixel = u16::from(bitmap.format.bytes_per_pixel()); + let row_len = bitmap.width.get() * bytes_per_pixel; + let chunk_height = u16::MAX / row_len; + + let mut cursor = WriteCursor::new(output); + let stride = bitmap.stride.get(); + let chunks = bitmap.data.chunks(stride * usize::from(chunk_height)); + + let total = cast_int!("chunks length lower bound", chunks.size_hint().0).map_err(BitmapEncodeError::Encode)?; + BitmapUpdateData::encode_header(total, &mut cursor).map_err(BitmapEncodeError::Encode)?; + + for (i, chunk) in chunks.enumerate() { + let height = cast_int!("bitmap height", chunk.len() / stride).map_err(BitmapEncodeError::Encode)?; + let i: u16 = cast_int!("chunk idx", i).map_err(BitmapEncodeError::Encode)?; + let top = bitmap.y + i * chunk_height; + + let encoder = BitmapStreamEncoder::new(NonZeroUsize::from(bitmap.width).get(), usize::from(height)); + + let len = { + let pixels = chunk + .chunks(stride) + .map(|row| &row[..usize::from(row_len)]) + .rev() + .flat_map(|row| row.chunks(usize::from(bytes_per_pixel))); + + Self::encode_iter(encoder, bitmap.format, pixels, self.buffer.as_mut_slice())? + }; + + let data = BitmapData { + rectangle: InclusiveRectangle { + left: bitmap.x, + top, + right: bitmap.x + bitmap.width.get() - 1, + bottom: top + height - 1, + }, + width: u16::from(bitmap.width), + height, + bits_per_pixel: u16::from(bitmap.format.bytes_per_pixel()) * 8, + compression_flags: Compression::BITMAP_COMPRESSION, + compressed_data_header: Some(bitmap::CompressedDataHeader { + main_body_size: cast_length!("main body size", len).map_err(BitmapEncodeError::Encode)?, + scan_width: u16::from(bitmap.width), + uncompressed_size: height * row_len, + }), + bitmap_data: &self.buffer[..len], + }; + + data.encode(&mut cursor).map_err(BitmapEncodeError::Encode)?; + } + + Ok(cursor.pos()) + } + + fn encode_iter<'a, P>( + mut encoder: BitmapStreamEncoder, + format: PixelFormat, + src: P, + dst: &mut [u8], + ) -> Result + where + P: Iterator + Clone, + { + let written = match format { + PixelFormat::ARgb32 | PixelFormat::XRgb32 => { + encoder.encode_pixels_stream::<_, ARgbChannels>(src, dst, true)? + } + PixelFormat::RgbA32 | PixelFormat::RgbX32 => { + encoder.encode_pixels_stream::<_, RgbAChannels>(src, dst, true)? + } + PixelFormat::ABgr32 | PixelFormat::XBgr32 => { + encoder.encode_pixels_stream::<_, ABgrChannels>(src, dst, true)? + } + PixelFormat::BgrA32 | PixelFormat::BgrX32 => { + encoder.encode_pixels_stream::<_, BgrAChannels>(src, dst, true)? + } + }; + + Ok(written) + } +} diff --git a/crates/ironrdp-server/src/encoder/fast_path.rs b/crates/ironrdp-server/src/encoder/fast_path.rs new file mode 100644 index 00000000..1b4da818 --- /dev/null +++ b/crates/ironrdp-server/src/encoder/fast_path.rs @@ -0,0 +1,193 @@ +use core::{cmp, fmt}; + +use ironrdp_pdu::fast_path::{EncryptionFlags, FastPathHeader, FastPathUpdatePdu, Fragmentation, UpdateCode}; +use ironrdp_pdu::{Encode as _, WriteCursor}; + +// this is the maximum amount of data (not including headers) we can send in a single TS_FP_UPDATE_PDU +const MAX_FASTPATH_UPDATE_SIZE: usize = 16_374; + +const FASTPATH_HEADER_SIZE: usize = 6; + +#[expect( + clippy::allow_attributes, + reason = "Unfortunately, expect attribute doesn't work when above or after visibility::make attribute" +)] +#[allow(unreachable_pub)] +#[expect( + clippy::partial_pub_fields, + reason = "public field is not a part of the public API and is used by benchmarks" +)] +#[cfg_attr(feature = "__bench", visibility::make(pub))] +pub(crate) struct UpdateFragmenter { + code: UpdateCode, + index: usize, + #[doc(hidden)] // not part of the public API, used by benchmarks + pub data: Vec, + position: usize, +} + +impl fmt::Debug for UpdateFragmenter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UpdateFragmenter") + .field("len", &self.data.len()) + .finish() + } +} + +impl UpdateFragmenter { + pub(crate) fn new(code: UpdateCode, data: Vec) -> Self { + Self { + code, + index: 0, + data, + position: 0, + } + } + + pub(crate) fn size_hint(&self) -> usize { + FASTPATH_HEADER_SIZE + cmp::min(self.data.len(), MAX_FASTPATH_UPDATE_SIZE) + } + + pub(crate) fn next(&mut self, dst: &mut [u8]) -> Option { + let (consumed, written) = self.encode_next(dst)?; + self.position += consumed; + self.index = self.index.checked_add(1)?; + Some(written) + } + + fn encode_next(&mut self, dst: &mut [u8]) -> Option<(usize, usize)> { + match self.data.len() - self.position { + 0 => None, + + 1..=MAX_FASTPATH_UPDATE_SIZE => { + let frag = if self.index > 0 { + Fragmentation::Last + } else { + Fragmentation::Single + }; + + self.encode_fastpath(frag, &self.data[self.position..], dst) + .map(|written| (self.data.len() - self.position, written)) + } + + _ => { + let frag = if self.index > 0 { + Fragmentation::Next + } else { + Fragmentation::First + }; + + self.encode_fastpath( + frag, + &self.data[self.position..MAX_FASTPATH_UPDATE_SIZE + self.position], + dst, + ) + .map(|written| (MAX_FASTPATH_UPDATE_SIZE, written)) + } + } + } + + fn encode_fastpath(&self, frag: Fragmentation, data: &[u8], dst: &mut [u8]) -> Option { + let mut cursor = WriteCursor::new(dst); + + let update = FastPathUpdatePdu { + fragmentation: frag, + update_code: self.code, + compression_flags: None, + compression_type: None, + data, + }; + + let header = FastPathHeader::new(EncryptionFlags::empty(), update.size()); + + header.encode(&mut cursor).ok()?; + update.encode(&mut cursor).ok()?; + + Some(cursor.pos()) + } +} + +#[cfg(test)] +mod tests { + use ironrdp_core::{decode_cursor, ReadCursor}; + + use super::*; + + #[test] + fn test_single_fragment() { + let data = vec![1, 2, 3, 4]; + let mut fragmenter = UpdateFragmenter::new(UpdateCode::Bitmap, data); + let mut buffer = vec![0; 100]; + let written = fragmenter.next(&mut buffer).unwrap(); + assert!(written > 0); + assert_eq!(fragmenter.index, 1); + + let mut cursor = ReadCursor::new(&buffer); + let header: FastPathHeader = decode_cursor(&mut cursor).unwrap(); + let update: FastPathUpdatePdu<'_> = decode_cursor(&mut cursor).unwrap(); + assert!(matches!(header, FastPathHeader { data_length: 7, .. })); + assert!(matches!( + update, + FastPathUpdatePdu { + fragmentation: Fragmentation::Single, + .. + } + )); + + assert!(fragmenter.next(&mut buffer).is_none()); + } + + #[test] + fn test_multi_fragment() { + let data = vec![0u8; MAX_FASTPATH_UPDATE_SIZE * 2 + 10]; + let mut fragmenter = UpdateFragmenter::new(UpdateCode::Bitmap, data); + let mut buffer = vec![0u8; fragmenter.size_hint()]; + let written = fragmenter.next(&mut buffer).unwrap(); + assert!(written > 0); + assert_eq!(fragmenter.index, 1); + + let mut cursor = ReadCursor::new(&buffer); + let _header: FastPathHeader = decode_cursor(&mut cursor).unwrap(); + let update: FastPathUpdatePdu<'_> = decode_cursor(&mut cursor).unwrap(); + assert!(matches!( + update, + FastPathUpdatePdu { + fragmentation: Fragmentation::First, + .. + } + )); + assert_eq!(update.data.len(), MAX_FASTPATH_UPDATE_SIZE); + + let written = fragmenter.next(&mut buffer).unwrap(); + assert!(written > 0); + assert_eq!(fragmenter.index, 2); + let mut cursor = ReadCursor::new(&buffer); + let _header: FastPathHeader = decode_cursor(&mut cursor).unwrap(); + let update: FastPathUpdatePdu<'_> = decode_cursor(&mut cursor).unwrap(); + assert!(matches!( + update, + FastPathUpdatePdu { + fragmentation: Fragmentation::Next, + .. + } + )); + assert_eq!(update.data.len(), MAX_FASTPATH_UPDATE_SIZE); + + let written = fragmenter.next(&mut buffer).unwrap(); + assert!(written > 0); + assert_eq!(fragmenter.index, 3); + let mut cursor = ReadCursor::new(&buffer); + let _header: FastPathHeader = decode_cursor(&mut cursor).unwrap(); + let update: FastPathUpdatePdu<'_> = decode_cursor(&mut cursor).unwrap(); + assert!(matches!( + update, + FastPathUpdatePdu { + fragmentation: Fragmentation::Last, + .. + } + )); + assert_eq!(update.data.len(), 10); + + assert!(fragmenter.next(&mut buffer).is_none()); + } +} diff --git a/crates/ironrdp-server/src/encoder/mod.rs b/crates/ironrdp-server/src/encoder/mod.rs new file mode 100644 index 00000000..6355c232 --- /dev/null +++ b/crates/ironrdp-server/src/encoder/mod.rs @@ -0,0 +1,624 @@ +use core::fmt; +use core::num::NonZeroU16; + +use anyhow::{anyhow, Context as _, Result}; +use ironrdp_acceptor::DesktopSize; +use ironrdp_graphics::diff::{find_different_rects_sub, Rect}; +use ironrdp_pdu::encode_vec; +use ironrdp_pdu::fast_path::UpdateCode; +use ironrdp_pdu::geometry::ExclusiveRectangle; +use ironrdp_pdu::pointer::{ColorPointerAttribute, Point16, PointerAttribute, PointerPositionAttribute}; +use ironrdp_pdu::rdp::capability_sets::{CmdFlags, EntropyBits}; +use ironrdp_pdu::surface_commands::{ExtendedBitmapDataPdu, SurfaceBitsPdu, SurfaceCommand}; +use tracing::{debug, warn}; + +use self::bitmap::BitmapEncoder; +use self::rfx::RfxEncoder; +use super::BitmapUpdate; +use crate::macros::time_warn; +use crate::{ColorPointer, DisplayUpdate, Framebuffer, RGBAPointer}; + +mod bitmap; +mod fast_path; +pub(crate) mod rfx; + +pub(crate) use fast_path::*; +use ironrdp_graphics::rdp6::BitmapEncodeError; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(u8)] +enum CodecId { + None = 0x0, +} + +impl CodecId { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u8(self) -> u8 { + self as u8 + } +} + +#[cfg_attr(feature = "__bench", visibility::make(pub))] +#[derive(Debug)] +pub(crate) struct UpdateEncoderCodecs { + remotefx: Option<(EntropyBits, u8)>, + #[cfg(feature = "qoi")] + qoi: Option, + #[cfg(feature = "qoiz")] + qoiz: Option, +} + +impl UpdateEncoderCodecs { + #[cfg_attr(feature = "__bench", visibility::make(pub))] + pub(crate) fn new() -> Self { + Self { + remotefx: None, + #[cfg(feature = "qoi")] + qoi: None, + #[cfg(feature = "qoiz")] + qoiz: None, + } + } + + #[cfg_attr(feature = "__bench", visibility::make(pub))] + pub(crate) fn set_remotefx(&mut self, remotefx: Option<(EntropyBits, u8)>) { + self.remotefx = remotefx + } + + #[cfg(feature = "qoi")] + #[cfg_attr(feature = "__bench", visibility::make(pub))] + pub(crate) fn set_qoi(&mut self, qoi: Option) { + self.qoi = qoi + } + + #[cfg(feature = "qoiz")] + #[cfg_attr(feature = "__bench", visibility::make(pub))] + pub(crate) fn set_qoiz(&mut self, qoiz: Option) { + self.qoiz = qoiz + } +} + +impl Default for UpdateEncoderCodecs { + fn default() -> Self { + Self::new() + } +} + +#[cfg_attr(feature = "__bench", visibility::make(pub))] +pub(crate) struct UpdateEncoder { + desktop_size: DesktopSize, + framebuffer: Option, + bitmap_updater: Option, +} + +impl fmt::Debug for UpdateEncoder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UpdateEncoder") + .field("bitmap_update", &self.bitmap_updater) + .finish() + } +} + +impl UpdateEncoder { + #[cfg_attr(feature = "__bench", visibility::make(pub))] + pub(crate) fn new(desktop_size: DesktopSize, surface_flags: CmdFlags, codecs: UpdateEncoderCodecs) -> Result { + let bitmap_updater = if surface_flags.contains(CmdFlags::SET_SURFACE_BITS) { + let mut bitmap = BitmapUpdater::None(NoneHandler); + + if let Some((algo, id)) = codecs.remotefx { + bitmap = BitmapUpdater::RemoteFx(RemoteFxHandler::new(algo, id, desktop_size)); + } + + #[cfg(feature = "qoi")] + if let Some(id) = codecs.qoi { + bitmap = BitmapUpdater::Qoi(QoiHandler::new(id)); + } + #[cfg(feature = "qoiz")] + if let Some(id) = codecs.qoiz { + bitmap = BitmapUpdater::Qoiz(QoizHandler::new(id).context("failed to initialize qoiz handler")?); + } + + bitmap + } else { + BitmapUpdater::Bitmap(BitmapHandler::new()) + }; + + Ok(Self { + desktop_size, + framebuffer: None, + bitmap_updater: Some(bitmap_updater), + }) + } + + #[cfg_attr(feature = "__bench", visibility::make(pub))] + pub(crate) fn update(&mut self, update: DisplayUpdate) -> EncoderIter<'_> { + EncoderIter { + encoder: self, + state: State::Start(update), + } + } + + pub(crate) fn set_desktop_size(&mut self, size: DesktopSize) { + self.desktop_size = size; + self.bitmap_updater + .as_mut() + .expect("bitmap updater always Some") + .set_desktop_size(size); + } + + fn rgba_pointer(ptr: RGBAPointer) -> Result { + let xor_mask = ptr.data; + + let hot_spot = Point16 { + x: ptr.hot_x, + y: ptr.hot_y, + }; + let color_pointer = ColorPointerAttribute { + cache_index: 0, + hot_spot, + width: ptr.width, + height: ptr.height, + xor_mask: &xor_mask, + and_mask: &[], + }; + let ptr = PointerAttribute { + xor_bpp: 32, + color_pointer, + }; + Ok(UpdateFragmenter::new(UpdateCode::NewPointer, encode_vec(&ptr)?)) + } + + fn color_pointer(ptr: ColorPointer) -> Result { + let hot_spot = Point16 { + x: ptr.hot_x, + y: ptr.hot_y, + }; + let ptr = ColorPointerAttribute { + cache_index: 0, + hot_spot, + width: ptr.width, + height: ptr.height, + xor_mask: &ptr.xor_mask, + and_mask: &ptr.and_mask, + }; + Ok(UpdateFragmenter::new(UpdateCode::ColorPointer, encode_vec(&ptr)?)) + } + + fn default_pointer() -> Result { + Ok(UpdateFragmenter::new(UpdateCode::DefaultPointer, vec![])) + } + + fn hide_pointer() -> Result { + Ok(UpdateFragmenter::new(UpdateCode::HiddenPointer, vec![])) + } + + fn pointer_position(pos: PointerPositionAttribute) -> Result { + Ok(UpdateFragmenter::new(UpdateCode::PositionPointer, encode_vec(&pos)?)) + } + + fn bitmap_diffs(&mut self, bitmap: &BitmapUpdate) -> Vec { + // TODO: we may want to make it optional for servers that already provide damaged regions + const USE_DIFFS: bool = true; + + if let Some(Framebuffer { + data, + stride, + width, + height, + .. + }) = USE_DIFFS.then_some(self.framebuffer.as_ref()).flatten() + { + find_different_rects_sub::<4>( + data, + *stride, + width.get().into(), + height.get().into(), + &bitmap.data, + bitmap.stride.get(), + bitmap.width.get().into(), + bitmap.height.get().into(), + bitmap.x.into(), + bitmap.y.into(), + ) + } else { + vec![Rect { + x: 0, + y: 0, + width: bitmap.width.get().into(), + height: bitmap.height.get().into(), + }] + } + } + + fn bitmap_update_framebuffer(&mut self, bitmap: BitmapUpdate, diffs: &[Rect]) { + if bitmap.x == 0 + && bitmap.y == 0 + && bitmap.width.get() == self.desktop_size.width + && bitmap.height.get() == self.desktop_size.height + { + match bitmap.try_into() { + Ok(framebuffer) => self.framebuffer = Some(framebuffer), + Err(err) => warn!("Failed to convert bitmap to framebuffer: {}", err), + } + } else if let Some(fb) = self.framebuffer.as_mut() { + fb.update_diffs(&bitmap, diffs); + } + } + + async fn bitmap(&mut self, bitmap: BitmapUpdate) -> Result { + // Move the bitmap updater to satisfy spawn_blocking 'static requirement. + // It is restored after the blocking operation completes. + let mut updater = self.bitmap_updater.take().expect("bitmap updater always Some"); + + let (result, updater) = tokio::task::spawn_blocking(move || { + let result = time_warn!("Encoding bitmap", 10, updater.handle(&bitmap)); + (result, updater) + }) + .await?; + + self.bitmap_updater = Some(updater); + + result + } +} + +#[derive(Debug, Default)] +enum State { + Start(DisplayUpdate), + BitmapDiffs { + diffs: Vec, + bitmap: BitmapUpdate, + pos: usize, + }, + #[default] + Ended, +} + +#[cfg_attr(feature = "__bench", visibility::make(pub))] +pub(crate) struct EncoderIter<'a> { + encoder: &'a mut UpdateEncoder, + state: State, +} + +impl EncoderIter<'_> { + #[cfg_attr(feature = "__bench", visibility::make(pub))] + pub(crate) async fn next(&mut self) -> Option> { + loop { + let state = core::mem::take(&mut self.state); + let encoder = &mut self.encoder; + + let res = match state { + State::Start(update) => match update { + DisplayUpdate::Bitmap(bitmap) => { + let diffs = encoder.bitmap_diffs(&bitmap); + self.state = State::BitmapDiffs { diffs, bitmap, pos: 0 }; + continue; + } + DisplayUpdate::PointerPosition(pos) => UpdateEncoder::pointer_position(pos), + DisplayUpdate::RGBAPointer(ptr) => UpdateEncoder::rgba_pointer(ptr), + DisplayUpdate::ColorPointer(ptr) => UpdateEncoder::color_pointer(ptr), + DisplayUpdate::HidePointer => UpdateEncoder::hide_pointer(), + DisplayUpdate::DefaultPointer => UpdateEncoder::default_pointer(), + DisplayUpdate::Resize(_) => return None, + }, + State::BitmapDiffs { diffs, bitmap, pos } => { + let Some(rect) = diffs.get(pos) else { + encoder.bitmap_update_framebuffer(bitmap, &diffs); + self.state = State::Ended; + return None; + }; + let Rect { x, y, width, height } = *rect; + + let x = match u16::try_from(x) { + Ok(x) => x, + Err(_) => return Some(Err(anyhow!("invalid `x`: out of range integral conversion"))), + }; + let y = match u16::try_from(y) { + Ok(y) => y, + Err(_) => return Some(Err(anyhow!("invalid `y`: out of range integral conversion"))), + }; + let width = match u16::try_from(width) { + Ok(width) => match NonZeroU16::new(width) { + Some(width) => width, + None => return Some(Err(anyhow!("rectangle width cannot be zero"))), + }, + Err(_) => return Some(Err(anyhow!("invalid `width`: out of range integral conversion"))), + }; + let height = match u16::try_from(height) { + Ok(height) => match NonZeroU16::new(height) { + Some(height) => height, + None => return Some(Err(anyhow!("rectangle height cannot be zero"))), + }, + Err(_) => return Some(Err(anyhow!("invalid `height`: out of range integral conversion"))), + }; + + let Some(sub) = bitmap.sub(x, y, width, height) else { + warn!("Failed to extract bitmap subregion"); + return None; + }; + self.state = State::BitmapDiffs { + diffs, + bitmap, + pos: pos + 1, + }; + encoder.bitmap(sub).await + } + State::Ended => return None, + }; + + return Some(res); + } + } +} + +#[derive(Debug)] +enum BitmapUpdater { + None(NoneHandler), + Bitmap(BitmapHandler), + RemoteFx(RemoteFxHandler), + #[cfg(feature = "qoi")] + Qoi(QoiHandler), + #[cfg(feature = "qoiz")] + Qoiz(QoizHandler), +} + +impl BitmapUpdater { + fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { + match self { + Self::None(up) => up.handle(bitmap), + Self::Bitmap(up) => up.handle(bitmap), + Self::RemoteFx(up) => up.handle(bitmap), + #[cfg(feature = "qoi")] + Self::Qoi(up) => up.handle(bitmap), + #[cfg(feature = "qoiz")] + Self::Qoiz(up) => up.handle(bitmap), + } + } + + fn set_desktop_size(&mut self, size: DesktopSize) { + if let Self::RemoteFx(up) = self { + up.set_desktop_size(size) + } + } +} + +trait BitmapUpdateHandler { + fn handle(&mut self, bitmap: &BitmapUpdate) -> Result; +} + +#[derive(Clone, Debug)] +struct NoneHandler; + +impl BitmapUpdateHandler for NoneHandler { + fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { + let stride = usize::from(bitmap.format.bytes_per_pixel()) * usize::from(bitmap.width.get()); + let mut data = Vec::with_capacity(stride * usize::from(bitmap.height.get())); + for row in bitmap.data.chunks(bitmap.stride.get()).rev() { + data.extend_from_slice(&row[..stride]); + } + set_surface(bitmap, CodecId::None.as_u8(), &data) + } +} + +#[derive(Clone)] +struct BitmapHandler { + bitmap: BitmapEncoder, +} + +impl fmt::Debug for BitmapHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BitmapHandler").finish() + } +} + +impl BitmapHandler { + fn new() -> Self { + Self { + bitmap: BitmapEncoder::new(), + } + } +} + +impl BitmapUpdateHandler for BitmapHandler { + fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { + let mut buffer = vec![0; bitmap.data.len() * 2]; // TODO: estimate bitmap encoded size + let len = loop { + match self.bitmap.encode(bitmap, buffer.as_mut_slice()) { + Err(err) => match err { + BitmapEncodeError::Encode(e) => match e.kind() { + ironrdp_core::EncodeErrorKind::NotEnoughBytes { .. } => { + buffer.resize(buffer.len() * 2, 0); + debug!("encoder buffer resized to: {}", buffer.len() * 2); + } + _ => Err(e).context("bitmap encode error")?, + }, + BitmapEncodeError::Rle(e) => Err(e).context("bitmap RLE encode error")?, + }, + Ok(len) => break len, + } + }; + + buffer.truncate(len); + Ok(UpdateFragmenter::new(UpdateCode::Bitmap, buffer)) + } +} + +#[derive(Debug, Clone)] +struct RemoteFxHandler { + remotefx: RfxEncoder, + codec_id: u8, + desktop_size: Option, +} + +impl RemoteFxHandler { + fn new(algo: EntropyBits, codec_id: u8, desktop_size: DesktopSize) -> Self { + Self { + remotefx: RfxEncoder::new(algo), + desktop_size: Some(desktop_size), + codec_id, + } + } + + fn set_desktop_size(&mut self, size: DesktopSize) { + self.desktop_size = Some(size); + } +} + +impl BitmapUpdateHandler for RemoteFxHandler { + fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { + let mut buffer = vec![0; bitmap.data.len()]; + let len = loop { + match self + .remotefx + .encode(bitmap, buffer.as_mut_slice(), self.desktop_size.take()) + { + Err(e) => match e.kind() { + ironrdp_core::EncodeErrorKind::NotEnoughBytes { .. } => { + buffer.resize(buffer.len() * 2, 0); + debug!("encoder buffer resized to: {}", buffer.len() * 2); + } + _ => Err(e).context("RemoteFX encode error")?, + }, + Ok(len) => break len, + } + }; + + set_surface(bitmap, self.codec_id, &buffer[..len]) + } +} + +#[cfg(feature = "qoi")] +#[derive(Clone, Debug)] +struct QoiHandler { + codec_id: u8, +} + +#[cfg(feature = "qoi")] +impl QoiHandler { + fn new(codec_id: u8) -> Self { + Self { codec_id } + } +} + +#[cfg(feature = "qoi")] +impl BitmapUpdateHandler for QoiHandler { + fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { + let data = qoi_encode(bitmap)?; + set_surface(bitmap, self.codec_id, &data) + } +} + +#[cfg(feature = "qoiz")] +struct QoizHandler { + codec_id: u8, + zctxt: zstd_safe::CCtx<'static>, +} + +#[cfg(feature = "qoiz")] +impl fmt::Debug for QoizHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("QoizHandler").field("codec_id", &self.codec_id).finish() + } +} + +#[cfg(feature = "qoiz")] +impl QoizHandler { + fn new(codec_id: u8) -> Result { + let mut zctxt = zstd_safe::CCtx::default(); + + zctxt + .set_parameter(zstd_safe::CParameter::CompressionLevel(3)) + .map_err(|code| { + anyhow!( + "failed to set zstd compression level: {}", + zstd_safe::get_error_name(code) + ) + })?; + zctxt + .set_parameter(zstd_safe::CParameter::EnableLongDistanceMatching(true)) + .map_err(|code| { + anyhow!( + "failed to set zstd enable long distance matching: {}", + zstd_safe::get_error_name(code) + ) + })?; + + Ok(Self { codec_id, zctxt }) + } +} + +#[cfg(feature = "qoiz")] +impl BitmapUpdateHandler for QoizHandler { + fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { + let qoi = qoi_encode(bitmap)?; + let mut inb = zstd_safe::InBuffer::around(&qoi); + let mut data = vec![0; qoi.len()]; + let mut outb; + let mut pos = 0; + + loop { + outb = zstd_safe::OutBuffer::around_pos(data.as_mut_slice(), pos); + let res = self + .zctxt + .compress_stream2( + &mut outb, + &mut inb, + zstd_safe::zstd_sys::ZSTD_EndDirective::ZSTD_e_flush, + ) + .map_err(|code| anyhow!("failed to Zstd compress: {}", zstd_safe::get_error_name(code)))?; + if res == 0 { + break; + } + pos = outb.pos(); + data.resize(data.len() + res, 0); + } + + set_surface(bitmap, self.codec_id, outb.as_slice()) + } +} + +#[cfg(feature = "qoi")] +fn qoi_encode(bitmap: &BitmapUpdate) -> Result> { + use ironrdp_graphics::image_processing::PixelFormat::*; + let raw_channels = match bitmap.format { + ARgb32 => qoi::RawChannels::Argb, + XRgb32 => qoi::RawChannels::Xrgb, + ABgr32 => qoi::RawChannels::Abgr, + XBgr32 => qoi::RawChannels::Xbgr, + BgrA32 => qoi::RawChannels::Bgra, + BgrX32 => qoi::RawChannels::Bgrx, + RgbA32 => qoi::RawChannels::Rgba, + RgbX32 => qoi::RawChannels::Rgbx, + }; + let enc = qoi::EncoderBuilder::new(&bitmap.data, bitmap.width.get().into(), bitmap.height.get().into()) + .stride(bitmap.stride.get()) + .raw_channels(raw_channels) + .build()?; + Ok(enc.encode_to_vec()?) +} + +fn set_surface(bitmap: &BitmapUpdate, codec_id: u8, data: &[u8]) -> Result { + let destination = ExclusiveRectangle { + left: bitmap.x, + top: bitmap.y, + right: bitmap.x + bitmap.width.get(), + bottom: bitmap.y + bitmap.height.get(), + }; + let extended_bitmap_data = ExtendedBitmapDataPdu { + bpp: bitmap.format.bytes_per_pixel() * 8, + width: bitmap.width.get(), + height: bitmap.height.get(), + codec_id, + header: None, + data, + }; + let pdu = SurfaceBitsPdu { + destination, + extended_bitmap_data, + }; + let cmd = SurfaceCommand::SetSurfaceBits(pdu); + Ok(UpdateFragmenter::new(UpdateCode::SurfaceCommands, encode_vec(&cmd)?)) +} diff --git a/crates/ironrdp-server/src/encoder/rfx.rs b/crates/ironrdp-server/src/encoder/rfx.rs new file mode 100644 index 00000000..198cec1d --- /dev/null +++ b/crates/ironrdp-server/src/encoder/rfx.rs @@ -0,0 +1,245 @@ +use std::io; + +use ironrdp_acceptor::DesktopSize; +use ironrdp_core::{cast_int, cast_length, other_err, Encode as _, EncodeResult}; +use ironrdp_graphics::color_conversion::to_64x64_ycbcr_tile; +use ironrdp_graphics::rfx_encode_component; +use ironrdp_graphics::rlgr::RlgrError; +use ironrdp_pdu::codecs::rfx::{ + self, Block, ChannelsPdu, CodecChannel, CodecVersionsPdu, FrameBeginPdu, FrameEndPdu, OperatingMode, Quant, + RegionPdu, RfxChannel, SyncPdu, TileSetPdu, +}; +use ironrdp_pdu::rdp::capability_sets::EntropyBits; +use ironrdp_pdu::WriteCursor; + +use crate::BitmapUpdate; + +#[derive(Debug, Clone)] +pub(crate) struct RfxEncoder { + entropy_algorithm: rfx::EntropyAlgorithm, +} + +impl RfxEncoder { + pub(crate) fn new(entropy_bits: EntropyBits) -> Self { + let entropy_algorithm = match entropy_bits { + EntropyBits::Rlgr1 => rfx::EntropyAlgorithm::Rlgr1, + EntropyBits::Rlgr3 => rfx::EntropyAlgorithm::Rlgr3, + }; + Self { entropy_algorithm } + } + + pub(crate) fn encode( + &mut self, + bitmap: &BitmapUpdate, + output: &mut [u8], + desktop_size: Option, + ) -> EncodeResult { + let mut cursor = WriteCursor::new(output); + let entropy_algorithm = self.entropy_algorithm; + + // header messages + if let Some(desktop_size) = desktop_size { + let width = desktop_size.width; + let height = desktop_size.height; + Block::Sync(SyncPdu).encode(&mut cursor)?; + let context = rfx::ContextPdu { + flags: OperatingMode::IMAGE_MODE, + entropy_algorithm, + }; + Block::CodecChannel(CodecChannel::Context(context)).encode(&mut cursor)?; + + let channels = ChannelsPdu(vec![RfxChannel { + width: cast_length!("width", width)?, + height: cast_length!("height", height)?, + }]); + Block::Channels(channels).encode(&mut cursor)?; + + Block::CodecVersions(CodecVersionsPdu).encode(&mut cursor)?; + } + + // data messages + let frame_begin = FrameBeginPdu { + index: 0, + number_of_regions: 1, + }; + Block::CodecChannel(CodecChannel::FrameBegin(frame_begin)).encode(&mut cursor)?; + + let width = bitmap.width.get(); + let height = bitmap.height.get(); + let rectangles = vec![rfx::RfxRectangle { + x: 0, + y: 0, + width, + height, + }]; + let region = RegionPdu { rectangles }; + Block::CodecChannel(CodecChannel::Region(region)).encode(&mut cursor)?; + + let quant = Quant::default(); + + let (encoder, mut data) = UpdateEncoder::new(bitmap, quant.clone(), entropy_algorithm); + let tiles = encoder.encode(&mut data)?; + + let quants = vec![quant]; + let tile_set = TileSetPdu { + entropy_algorithm, + quants, + tiles, + }; + Block::CodecChannel(CodecChannel::TileSet(tile_set)).encode(&mut cursor)?; + + let frame_end = FrameEndPdu; + Block::CodecChannel(CodecChannel::FrameEnd(frame_end)).encode(&mut cursor)?; + + Ok(cursor.pos()) + } +} + +pub(crate) struct UpdateEncoder<'a> { + bitmap: &'a BitmapUpdate, + quant: Quant, + entropy_algorithm: rfx::EntropyAlgorithm, +} + +struct UpdateEncoderData(Vec); + +struct EncodedTile<'a> { + y_data: &'a [u8], + cb_data: &'a [u8], + cr_data: &'a [u8], +} + +impl<'a> UpdateEncoder<'a> { + fn new( + bitmap: &'a BitmapUpdate, + quant: Quant, + entropy_algorithm: rfx::EntropyAlgorithm, + ) -> (Self, UpdateEncoderData) { + let this = Self { + bitmap, + quant, + entropy_algorithm, + }; + let data = this.alloc_data(); + + (this, data) + } + + fn alloc_data(&self) -> UpdateEncoderData { + let (tiles_x, tiles_y) = self.tiles_xy(); + + UpdateEncoderData(vec![0u8; 64 * 64 * 3 * tiles_x * tiles_y]) + } + + fn tiles_xy(&self) -> (usize, usize) { + ( + self.bitmap.width.get().div_ceil(64).into(), + self.bitmap.height.get().div_ceil(64).into(), + ) + } + + fn encode(&self, data: &'a mut UpdateEncoderData) -> EncodeResult>> { + #[cfg(feature = "rayon")] + use rayon::prelude::*; + + let (tiles_x, tiles_y) = self.tiles_xy(); + + #[cfg(not(feature = "rayon"))] + let chunks = data.0.chunks_mut(64 * 64 * 3); + #[cfg(feature = "rayon")] + let chunks = data.0.par_chunks_mut(64 * 64 * 3); + + let tiles: Vec<_> = (0..tiles_y).flat_map(|y| (0..tiles_x).map(move |x| (x, y))).collect(); + + chunks + .zip(tiles) + .map(|(buf, (tile_x, tile_y))| { + let EncodedTile { + y_data, + cb_data, + cr_data, + } = self + .encode_tile(tile_x, tile_y, buf) + .map_err(|e| other_err!("rfxenc", source: e))?; + + let tile = rfx::Tile { + y_quant_index: 0, + cb_quant_index: 0, + cr_quant_index: 0, + x: cast_int!("tile_x", tile_x)?, + y: cast_int!("tile_y", tile_y)?, + y_data, + cb_data, + cr_data, + }; + Ok(tile) + }) + .collect() + } + + fn encode_tile<'b>(&self, tile_x: usize, tile_y: usize, buf: &'b mut [u8]) -> Result, RlgrError> { + #![allow(clippy::similar_names)] // It’s hard to find better names for cr, cb, etc. + + assert!(buf.len() >= 4096 * 3); + + let bpp: usize = self.bitmap.format.bytes_per_pixel().into(); + let width: usize = self.bitmap.width.get().into(); + let height: usize = self.bitmap.height.get().into(); + + let x = tile_x * 64; + let y = tile_y * 64; + let tile_width = u32::try_from(core::cmp::min(width - x, 64)).expect("can always fit in u32"); + let tile_height = u32::try_from(core::cmp::min(height - y, 64)).expect("can always fit in u32"); + let stride = self.bitmap.stride.get(); + let input = &self.bitmap.data[y * stride + x * bpp..]; + + let stride = u32::try_from(stride).map_err(io::Error::other)?; + let y = &mut [0i16; 4096]; + let cb = &mut [0i16; 4096]; + let cr = &mut [0i16; 4096]; + + to_64x64_ycbcr_tile(input, tile_width, tile_height, stride, self.bitmap.format, y, cb, cr) + .map_err(RlgrError::Yuv)?; + + let (y_data, buf) = buf.split_at_mut(4096); + let (cb_data, cr_data) = buf.split_at_mut(4096); + + let len = rfx_encode_component(y, y_data, &self.quant, self.entropy_algorithm)?; + let y_data = &y_data[..len]; + let len = rfx_encode_component(cb, cb_data, &self.quant, self.entropy_algorithm)?; + let cb_data = &cb_data[..len]; + let len = rfx_encode_component(cr, cr_data, &self.quant, self.entropy_algorithm)?; + let cr_data = &cr_data[..len]; + + Ok(EncodedTile { + y_data, + cb_data, + cr_data, + }) + } +} + +#[cfg(feature = "__bench")] +#[expect(clippy::missing_panics_doc, reason = "panics in benches are allowed")] +pub(crate) mod bench { + use super::*; + + pub fn rfx_enc_tile( + bitmap: &BitmapUpdate, + quant: &Quant, + algo: rfx::EntropyAlgorithm, + tile_x: usize, + tile_y: usize, + ) { + let (enc, mut data) = UpdateEncoder::new(bitmap, quant.clone(), algo); + + enc.encode_tile(tile_x, tile_y, &mut data.0) + .expect("cannot propagate error in benchmark"); + } + + pub fn rfx_enc(bitmap: &BitmapUpdate, quant: &Quant, algo: rfx::EntropyAlgorithm) { + let (enc, mut data) = UpdateEncoder::new(bitmap, quant.clone(), algo); + + enc.encode(&mut data).expect("cannot propagate error in benchmark"); + } +} diff --git a/crates/ironrdp-server/src/handler.rs b/crates/ironrdp-server/src/handler.rs new file mode 100644 index 00000000..0bcf6519 --- /dev/null +++ b/crates/ironrdp-server/src/handler.rs @@ -0,0 +1,276 @@ +use ironrdp_ainput as ainput; +use ironrdp_pdu::input::fast_path::{self, SynchronizeFlags}; +use ironrdp_pdu::input::mouse::PointerFlags; +use ironrdp_pdu::input::mouse_rel::PointerRelFlags; +use ironrdp_pdu::input::mouse_x::PointerXFlags; +use ironrdp_pdu::input::sync::SyncToggleFlags; +use ironrdp_pdu::input::{scan_code, unicode, MousePdu, MouseRelPdu, MouseXPdu}; + +/// Keyboard Event +/// +/// Describes a keyboard event received from the client +/// +#[derive(Debug)] +pub enum KeyboardEvent { + Pressed { code: u8, extended: bool }, + Released { code: u8, extended: bool }, + UnicodePressed(u16), + UnicodeReleased(u16), + Synchronize(SynchronizeFlags), +} + +/// Mouse Event +/// +/// Describes a mouse event received from the client +/// +#[derive(Debug)] +pub enum MouseEvent { + Move { x: u16, y: u16 }, + RightPressed, + RightReleased, + LeftPressed, + LeftReleased, + MiddlePressed, + MiddleReleased, + Button4Pressed, + Button4Released, + Button5Pressed, + Button5Released, + VerticalScroll { value: i16 }, + Scroll { x: i32, y: i32 }, + RelMove { x: i32, y: i32 }, +} + +/// Input Event Handler for an RDP server +/// +/// Whenever the RDP server will receive an input event from a client, the relevant callback from +/// this handler will be called +/// +/// # Example +/// +/// ``` +/// use ironrdp_server::{KeyboardEvent, MouseEvent, RdpServerInputHandler}; +/// +/// pub struct InputHandler; +/// +/// impl RdpServerInputHandler for InputHandler { +/// fn keyboard(&mut self, event: KeyboardEvent) { +/// match event { +/// KeyboardEvent::Pressed { code, .. } => println!("Pressed {}", code), +/// KeyboardEvent::Released { code, .. } => println!("Released {}", code), +/// other => println!("unhandled event: {:?}", other), +/// }; +/// } +/// +/// fn mouse(&mut self, event: MouseEvent) { +/// let result = match event { +/// MouseEvent::Move { x, y } => println!("Moved mouse to {} {}", x, y), +/// other => println!("unhandled event: {:?}", other), +/// }; +/// } +/// } +/// ``` +pub trait RdpServerInputHandler: Send { + fn keyboard(&mut self, event: KeyboardEvent); + fn mouse(&mut self, event: MouseEvent); +} + +impl From<(u8, fast_path::KeyboardFlags)> for KeyboardEvent { + fn from((key, flags): (u8, fast_path::KeyboardFlags)) -> Self { + let extended = flags.contains(fast_path::KeyboardFlags::EXTENDED); + if flags.contains(fast_path::KeyboardFlags::RELEASE) { + KeyboardEvent::Released { code: key, extended } + } else { + KeyboardEvent::Pressed { code: key, extended } + } + } +} + +impl From<(u16, fast_path::KeyboardFlags)> for KeyboardEvent { + fn from((key, flags): (u16, fast_path::KeyboardFlags)) -> Self { + if flags.contains(fast_path::KeyboardFlags::RELEASE) { + KeyboardEvent::UnicodeReleased(key) + } else { + KeyboardEvent::UnicodePressed(key) + } + } +} + +impl From<(u16, scan_code::KeyboardFlags)> for KeyboardEvent { + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "we are truncating the value on purpose" + )] + fn from((key, flags): (u16, scan_code::KeyboardFlags)) -> Self { + let extended = flags.contains(scan_code::KeyboardFlags::EXTENDED); + + if flags.contains(scan_code::KeyboardFlags::RELEASE) { + KeyboardEvent::Released { + code: key as u8, + extended, + } + } else { + KeyboardEvent::Pressed { + code: key as u8, + extended, + } + } + } +} + +impl From<(u16, unicode::KeyboardFlags)> for KeyboardEvent { + fn from((key, flags): (u16, unicode::KeyboardFlags)) -> Self { + if flags.contains(unicode::KeyboardFlags::RELEASE) { + KeyboardEvent::UnicodeReleased(key) + } else { + KeyboardEvent::UnicodePressed(key) + } + } +} + +impl From for KeyboardEvent { + fn from(value: SynchronizeFlags) -> Self { + KeyboardEvent::Synchronize(value) + } +} + +impl From for KeyboardEvent { + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "we are truncating the value on purpose" + )] + fn from(value: SyncToggleFlags) -> Self { + KeyboardEvent::Synchronize(SynchronizeFlags::from_bits_truncate(value.bits() as u8)) + } +} + +impl From for MouseEvent { + fn from(value: MousePdu) -> Self { + if value.flags.contains(PointerFlags::LEFT_BUTTON) { + if value.flags.contains(PointerFlags::DOWN) { + MouseEvent::LeftPressed + } else { + MouseEvent::LeftReleased + } + } else if value.flags.contains(PointerFlags::RIGHT_BUTTON) { + if value.flags.contains(PointerFlags::DOWN) { + MouseEvent::RightPressed + } else { + MouseEvent::RightReleased + } + } else if value.flags.contains(PointerFlags::VERTICAL_WHEEL) { + MouseEvent::VerticalScroll { + value: value.number_of_wheel_rotation_units, + } + } else { + MouseEvent::Move { + x: value.x_position, + y: value.y_position, + } + } + } +} + +impl From for MouseEvent { + fn from(value: MouseXPdu) -> Self { + if value.flags.contains(PointerXFlags::BUTTON1) { + if value.flags.contains(PointerXFlags::DOWN) { + MouseEvent::LeftPressed + } else { + MouseEvent::LeftReleased + } + } else if value.flags.contains(PointerXFlags::BUTTON2) { + if value.flags.contains(PointerXFlags::DOWN) { + MouseEvent::RightPressed + } else { + MouseEvent::RightReleased + } + } else { + MouseEvent::Move { + x: value.x_position, + y: value.y_position, + } + } + } +} + +impl From for MouseEvent { + fn from(value: MouseRelPdu) -> Self { + if value.flags.contains(PointerRelFlags::BUTTON1) { + if value.flags.contains(PointerRelFlags::DOWN) { + MouseEvent::LeftPressed + } else { + MouseEvent::LeftReleased + } + } else if value.flags.contains(PointerRelFlags::BUTTON2) { + if value.flags.contains(PointerRelFlags::DOWN) { + MouseEvent::RightPressed + } else { + MouseEvent::RightReleased + } + } else if value.flags.contains(PointerRelFlags::BUTTON3) { + if value.flags.contains(PointerRelFlags::DOWN) { + MouseEvent::MiddlePressed + } else { + MouseEvent::MiddleReleased + } + } else if value.flags.contains(PointerRelFlags::XBUTTON1) { + if value.flags.contains(PointerRelFlags::DOWN) { + MouseEvent::Button4Pressed + } else { + MouseEvent::Button4Released + } + } else if value.flags.contains(PointerRelFlags::XBUTTON2) { + if value.flags.contains(PointerRelFlags::DOWN) { + MouseEvent::Button5Pressed + } else { + MouseEvent::Button5Released + } + } else { + MouseEvent::RelMove { + x: value.x_delta.into(), + y: value.y_delta.into(), + } + } + } +} + +impl From for MouseEvent { + fn from(value: ainput::MousePdu) -> Self { + use ainput::MouseEventFlags; + + if value.flags.contains(MouseEventFlags::BUTTON1) { + if value.flags.contains(MouseEventFlags::DOWN) { + MouseEvent::LeftPressed + } else { + MouseEvent::LeftReleased + } + } else if value.flags.contains(MouseEventFlags::BUTTON2) { + if value.flags.contains(MouseEventFlags::DOWN) { + MouseEvent::RightPressed + } else { + MouseEvent::RightReleased + } + } else if value.flags.contains(MouseEventFlags::BUTTON3) { + if value.flags.contains(MouseEventFlags::DOWN) { + MouseEvent::MiddlePressed + } else { + MouseEvent::MiddleReleased + } + } else if value.flags.contains(MouseEventFlags::WHEEL) { + MouseEvent::Scroll { x: value.x, y: value.y } + } else if value.flags.contains(MouseEventFlags::REL) { + MouseEvent::RelMove { x: value.x, y: value.y } + } else if value.flags.contains(MouseEventFlags::MOVE) { + // assume moves are 0 <= u16::MAX + MouseEvent::Move { + x: value.x.try_into().unwrap_or(0), + y: value.y.try_into().unwrap_or(0), + } + } else { + MouseEvent::Move { x: 0, y: 0 } + } + } +} diff --git a/crates/ironrdp-server/src/helper.rs b/crates/ironrdp-server/src/helper.rs new file mode 100644 index 00000000..53d6870f --- /dev/null +++ b/crates/ironrdp-server/src/helper.rs @@ -0,0 +1,76 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::Path; +use std::sync::Arc; + +use anyhow::Context as _; +use rustls_pemfile::{certs, pkcs8_private_keys}; +use tokio_rustls::rustls::pki_types::pem::PemObject as _; +use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use tokio_rustls::{rustls, TlsAcceptor}; + +pub struct TlsIdentityCtx { + pub certs: Vec>, + pub priv_key: PrivateKeyDer<'static>, + pub pub_key: Vec, +} + +impl TlsIdentityCtx { + /// A constructor to create a `TlsIdentityCtx` from the given certificate and key paths. + /// + /// The file format can be either PEM (if the file extension ends with .pem) or DER. + pub fn init_from_paths(cert_path: &Path, key_path: &Path) -> anyhow::Result { + let certs = if cert_path.extension().is_some_and(|ext| ext == "pem") { + CertificateDer::pem_file_iter(cert_path) + .with_context(|| format!("reading server cert `{cert_path:?}`"))? + .collect::, _>>() + .with_context(|| format!("collecting server cert `{cert_path:?}`"))? + } else { + certs(&mut BufReader::new( + File::open(cert_path).with_context(|| format!("opening server cert `{cert_path:?}`"))?, + )) + .collect::, _>>() + .with_context(|| format!("collecting server cert `{cert_path:?}`"))? + }; + + let priv_key = if key_path.extension().is_some_and(|ext| ext == "pem") { + PrivateKeyDer::from_pem_file(key_path).with_context(|| format!("reading server key `{key_path:?}`"))? + } else { + pkcs8_private_keys(&mut BufReader::new(File::open(key_path)?)) + .next() + .context("no private key")? + .map(PrivateKeyDer::from)? + }; + + let pub_key = { + use x509_cert::der::Decode as _; + + let cert = certs.first().ok_or_else(|| std::io::Error::other("invalid cert"))?; + let cert = x509_cert::Certificate::from_der(cert).map_err(std::io::Error::other)?; + cert.tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() + .ok_or_else(|| std::io::Error::other("subject public key BIT STRING is not aligned"))? + .to_owned() + }; + + Ok(Self { + certs, + priv_key, + pub_key, + }) + } + + pub fn make_acceptor(&self) -> anyhow::Result { + let mut server_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(self.certs.clone(), self.priv_key.clone_key()) + .context("bad certificate/key")?; + + // This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret) + server_config.key_log = Arc::new(rustls::KeyLogFile::new()); + + Ok(TlsAcceptor::from(Arc::new(server_config))) + } +} diff --git a/crates/ironrdp-server/src/lib.rs b/crates/ironrdp-server/src/lib.rs new file mode 100644 index 00000000..bddb9a1a --- /dev/null +++ b/crates/ironrdp-server/src/lib.rs @@ -0,0 +1,37 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![allow(clippy::arithmetic_side_effects)] // TODO: should we enable this lint back? + +pub use {tokio, tokio_rustls}; + +mod macros; + +mod builder; +mod capabilities; +mod clipboard; +mod display; +mod encoder; +mod handler; +#[cfg(feature = "helper")] +mod helper; +mod server; +mod sound; + +pub use clipboard::*; +pub use display::*; +pub use handler::*; +#[cfg(feature = "helper")] +pub use helper::*; +pub use server::*; +pub use sound::*; + +#[cfg(feature = "__bench")] +pub mod bench { + pub mod encoder { + pub mod rfx { + pub use crate::encoder::rfx::bench::{rfx_enc, rfx_enc_tile}; + } + + pub use crate::encoder::{UpdateEncoder, UpdateEncoderCodecs}; + } +} diff --git a/crates/ironrdp-server/src/macros.rs b/crates/ironrdp-server/src/macros.rs new file mode 100644 index 00000000..5b1b60cd --- /dev/null +++ b/crates/ironrdp-server/src/macros.rs @@ -0,0 +1,24 @@ +macro_rules! time_warn { + ($context:expr, $threshold_ms:expr, $op:expr) => {{ + #[cold] + fn warn_log(context: &str, duration: u128) { + use ::core::sync::atomic::AtomicUsize; + + static COUNT: AtomicUsize = AtomicUsize::new(0); + let current_count = COUNT.fetch_add(1, ::core::sync::atomic::Ordering::Relaxed); + if current_count < 50 || current_count % 100 == 0 { + ::tracing::warn!("{context} took {duration} ms! (count: {current_count})"); + } + } + + let start = std::time::Instant::now(); + let result = $op; + let duration = start.elapsed().as_millis(); + if duration > $threshold_ms { + warn_log($context, duration); + } + result + }}; +} + +pub(crate) use time_warn; diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs new file mode 100644 index 00000000..9fe234d8 --- /dev/null +++ b/crates/ironrdp-server/src/server.rs @@ -0,0 +1,1063 @@ +use core::net::SocketAddr; +use std::rc::Rc; +use std::sync::Arc; + +use anyhow::{anyhow, bail, Context as _, Result}; +use ironrdp_acceptor::{Acceptor, AcceptorResult, BeginResult, DesktopSize}; +use ironrdp_async::Framed; +use ironrdp_cliprdr::backend::ClipboardMessage; +use ironrdp_cliprdr::CliprdrServer; +use ironrdp_core::{decode, encode_vec, impl_as_any}; +use ironrdp_displaycontrol::pdu::DisplayControlMonitorLayout; +use ironrdp_displaycontrol::server::{DisplayControlHandler, DisplayControlServer}; +use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent}; +use ironrdp_pdu::input::InputEventPdu; +use ironrdp_pdu::mcs::{SendDataIndication, SendDataRequest}; +use ironrdp_pdu::rdp::capability_sets::{BitmapCodecs, CapabilitySet, CmdFlags, CodecProperty, GeneralExtraFlags}; +pub use ironrdp_pdu::rdp::client_info::Credentials; +use ironrdp_pdu::rdp::headers::{ServerDeactivateAll, ShareControlPdu}; +use ironrdp_pdu::x224::X224; +use ironrdp_pdu::{decode_err, mcs, nego, rdp, Action, PduResult}; +use ironrdp_svc::{server_encode_svc_messages, StaticChannelId, StaticChannelSet, SvcProcessor}; +use ironrdp_tokio::{split_tokio_framed, unsplit_tokio_framed, FramedRead, FramedWrite, TokioFramed}; +use rdpsnd::server::{RdpsndServer, RdpsndServerMessage}; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::{mpsc, oneshot, Mutex}; +use tokio::task; +use tokio_rustls::TlsAcceptor; +use tracing::{debug, error, trace, warn}; +use {ironrdp_dvc as dvc, ironrdp_rdpsnd as rdpsnd}; + +use crate::clipboard::CliprdrServerFactory; +use crate::display::{DisplayUpdate, RdpServerDisplay}; +use crate::encoder::{UpdateEncoder, UpdateEncoderCodecs}; +use crate::handler::RdpServerInputHandler; +use crate::{builder, capabilities, SoundServerFactory}; + +#[derive(Clone)] +pub struct RdpServerOptions { + pub addr: SocketAddr, + pub security: RdpServerSecurity, + pub codecs: BitmapCodecs, +} + +impl RdpServerOptions { + fn has_image_remote_fx(&self) -> bool { + self.codecs + .0 + .iter() + .any(|codec| matches!(codec.property, CodecProperty::ImageRemoteFx(_))) + } + + fn has_remote_fx(&self) -> bool { + self.codecs + .0 + .iter() + .any(|codec| matches!(codec.property, CodecProperty::RemoteFx(_))) + } + + #[cfg(feature = "qoi")] + fn has_qoi(&self) -> bool { + self.codecs + .0 + .iter() + .any(|codec| matches!(codec.property, CodecProperty::Qoi)) + } + + #[cfg(feature = "qoiz")] + fn has_qoiz(&self) -> bool { + self.codecs + .0 + .iter() + .any(|codec| matches!(codec.property, CodecProperty::QoiZ)) + } +} + +#[derive(Clone)] +pub enum RdpServerSecurity { + None, + Tls(TlsAcceptor), + /// Used for both hybrid + hybrid-ex. + Hybrid((TlsAcceptor, Vec)), +} + +impl RdpServerSecurity { + pub fn flag(&self) -> nego::SecurityProtocol { + match self { + RdpServerSecurity::None => nego::SecurityProtocol::empty(), + RdpServerSecurity::Tls(_) => nego::SecurityProtocol::SSL, + RdpServerSecurity::Hybrid(_) => nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX, + } + } +} + +struct AInputHandler { + handler: Arc>>, +} + +impl_as_any!(AInputHandler); + +impl dvc::DvcProcessor for AInputHandler { + fn channel_name(&self) -> &str { + ironrdp_ainput::CHANNEL_NAME + } + + fn start(&mut self, _channel_id: u32) -> PduResult> { + use ironrdp_ainput::{ServerPdu, VersionPdu}; + + let pdu = ServerPdu::Version(VersionPdu::default()); + + Ok(vec![Box::new(pdu)]) + } + + fn close(&mut self, _channel_id: u32) {} + + fn process(&mut self, _channel_id: u32, payload: &[u8]) -> PduResult> { + use ironrdp_ainput::ClientPdu; + + match decode(payload).map_err(|e| decode_err!(e))? { + ClientPdu::Mouse(pdu) => { + let handler = Arc::clone(&self.handler); + task::spawn_blocking(move || { + handler.blocking_lock().mouse(pdu.into()); + }); + } + } + + Ok(Vec::new()) + } +} + +impl dvc::DvcServerProcessor for AInputHandler {} + +struct DisplayControlBackend { + display: Arc>>, +} + +impl DisplayControlBackend { + fn new(display: Arc>>) -> Self { + Self { display } + } +} + +impl DisplayControlHandler for DisplayControlBackend { + fn monitor_layout(&self, layout: DisplayControlMonitorLayout) { + let display = Arc::clone(&self.display); + task::spawn_blocking(move || display.blocking_lock().request_layout(layout)); + } +} + +/// RDP Server +/// +/// A server is created to listen for connections. +/// After the connection sequence is finalized using the provided security mechanism, the server can: +/// - receive display updates from a [`RdpServerDisplay`] and forward them to the client +/// - receive input events from a client and forward them to an [`RdpServerInputHandler`] +/// +/// # Example +/// +/// ``` +/// use ironrdp_server::{RdpServer, RdpServerInputHandler, RdpServerDisplay, RdpServerDisplayUpdates}; +/// +///# use anyhow::Result; +///# use ironrdp_server::{DisplayUpdate, DesktopSize, KeyboardEvent, MouseEvent}; +///# use tokio_rustls::TlsAcceptor; +///# struct NoopInputHandler; +///# impl RdpServerInputHandler for NoopInputHandler { +///# fn keyboard(&mut self, _: KeyboardEvent) {} +///# fn mouse(&mut self, _: MouseEvent) {} +///# } +///# struct NoopDisplay; +///# #[async_trait::async_trait] +///# impl RdpServerDisplay for NoopDisplay { +///# async fn size(&mut self) -> DesktopSize { +///# todo!() +///# } +///# async fn updates(&mut self) -> Result> { +///# todo!() +///# } +///# } +///# async fn stub() -> Result<()> { +/// fn make_tls_acceptor() -> TlsAcceptor { +/// /* snip */ +///# todo!() +/// } +/// +/// fn make_input_handler() -> impl RdpServerInputHandler { +/// /* snip */ +///# NoopInputHandler +/// } +/// +/// fn make_display_handler() -> impl RdpServerDisplay { +/// /* snip */ +///# NoopDisplay +/// } +/// +/// let tls_acceptor = make_tls_acceptor(); +/// let input_handler = make_input_handler(); +/// let display_handler = make_display_handler(); +/// +/// let mut server = RdpServer::builder() +/// .with_addr(([127, 0, 0, 1], 3389)) +/// .with_tls(tls_acceptor) +/// .with_input_handler(input_handler) +/// .with_display_handler(display_handler) +/// .build(); +/// +/// server.run().await; +/// Ok(()) +///# } +/// ``` +pub struct RdpServer { + opts: RdpServerOptions, + // FIXME: replace with a channel and poll/process the handler? + handler: Arc>>, + display: Arc>>, + static_channels: StaticChannelSet, + sound_factory: Option>, + cliprdr_factory: Option>, + ev_sender: mpsc::UnboundedSender, + ev_receiver: Arc>>, + creds: Option, + local_addr: Option, +} + +#[derive(Debug)] +pub enum ServerEvent { + Quit(String), + Clipboard(ClipboardMessage), + Rdpsnd(RdpsndServerMessage), + SetCredentials(Credentials), + GetLocalAddr(oneshot::Sender>), +} + +pub trait ServerEventSender { + fn set_sender(&mut self, sender: mpsc::UnboundedSender); +} + +impl ServerEvent { + pub fn create_channel() -> (mpsc::UnboundedSender, mpsc::UnboundedReceiver) { + mpsc::unbounded_channel() + } +} + +#[derive(Debug, PartialEq)] +enum RunState { + Continue, + Disconnect, + DeactivationReactivation { desktop_size: DesktopSize }, +} + +impl RdpServer { + pub fn new( + opts: RdpServerOptions, + handler: Box, + display: Box, + mut sound_factory: Option>, + mut cliprdr_factory: Option>, + ) -> Self { + let (ev_sender, ev_receiver) = ServerEvent::create_channel(); + if let Some(cliprdr) = cliprdr_factory.as_mut() { + cliprdr.set_sender(ev_sender.clone()); + } + if let Some(snd) = sound_factory.as_mut() { + snd.set_sender(ev_sender.clone()); + } + Self { + opts, + handler: Arc::new(Mutex::new(handler)), + display: Arc::new(Mutex::new(display)), + static_channels: StaticChannelSet::new(), + sound_factory, + cliprdr_factory, + ev_sender, + ev_receiver: Arc::new(Mutex::new(ev_receiver)), + creds: None, + local_addr: None, + } + } + + pub fn builder() -> builder::RdpServerBuilder { + builder::RdpServerBuilder::new() + } + + pub fn event_sender(&self) -> &mpsc::UnboundedSender { + &self.ev_sender + } + + fn attach_channels(&mut self, acceptor: &mut Acceptor) { + if let Some(cliprdr_factory) = self.cliprdr_factory.as_deref() { + let backend = cliprdr_factory.build_cliprdr_backend(); + + let cliprdr = CliprdrServer::new(backend); + + acceptor.attach_static_channel(cliprdr); + } + + if let Some(factory) = self.sound_factory.as_deref() { + let backend = factory.build_backend(); + + acceptor.attach_static_channel(RdpsndServer::new(backend)); + } + + let dcs_backend = DisplayControlBackend::new(Arc::clone(&self.display)); + let dvc = dvc::DrdynvcServer::new() + .with_dynamic_channel(AInputHandler { + handler: Arc::clone(&self.handler), + }) + .with_dynamic_channel(DisplayControlServer::new(Box::new(dcs_backend))); + acceptor.attach_static_channel(dvc); + } + + pub async fn run_connection(&mut self, stream: TcpStream) -> Result<()> { + let framed = TokioFramed::new(stream); + + let size = self.display.lock().await.size().await; + let capabilities = capabilities::capabilities(&self.opts, size); + let mut acceptor = Acceptor::new(self.opts.security.flag(), size, capabilities, self.creds.clone()); + + self.attach_channels(&mut acceptor); + + let res = ironrdp_acceptor::accept_begin(framed, &mut acceptor) + .await + .context("accept_begin failed")?; + + match res { + BeginResult::ShouldUpgrade(stream) => { + let tls_acceptor = match &self.opts.security { + RdpServerSecurity::Tls(acceptor) => acceptor, + RdpServerSecurity::Hybrid((acceptor, _)) => acceptor, + RdpServerSecurity::None => unreachable!(), + }; + let accept = match tls_acceptor.accept(stream).await { + Ok(accept) => accept, + Err(e) => { + warn!("Failed to TLS accept: {}", e); + return Ok(()); + } + }; + let mut framed = TokioFramed::new(accept); + + acceptor.mark_security_upgrade_as_done(); + + if let RdpServerSecurity::Hybrid((_, pub_key)) = &self.opts.security { + // how to get the client name? + // doesn't seem to matter yet + let client_name = framed.get_inner().0.get_ref().0.peer_addr()?.to_string(); + + ironrdp_acceptor::accept_credssp( + &mut framed, + &mut acceptor, + &mut ironrdp_tokio::reqwest::ReqwestNetworkClient::new(), + client_name.into(), + pub_key.clone(), + None, + ) + .await?; + } + + let framed = self.accept_finalize(framed, acceptor).await?; + debug!("Shutting down TLS connection"); + let (mut tls_stream, _) = framed.into_inner(); + if let Err(e) = tls_stream.shutdown().await { + debug!(?e, "TLS shutdown error"); + } + } + + BeginResult::Continue(framed) => { + self.accept_finalize(framed, acceptor).await?; + } + }; + + Ok(()) + } + + pub async fn run(&mut self) -> Result<()> { + let listener = TcpListener::bind(self.opts.addr).await?; + let local_addr = listener.local_addr()?; + + debug!("Listening for connections on {local_addr}"); + self.local_addr = Some(local_addr); + + loop { + let ev_receiver = Arc::clone(&self.ev_receiver); + let mut ev_receiver = ev_receiver.lock().await; + tokio::select! { + Some(event) = ev_receiver.recv() => { + match event { + ServerEvent::Quit(reason) => { + debug!("Got quit event {reason}"); + break; + } + ServerEvent::GetLocalAddr(tx) => { + let _ = tx.send(self.local_addr); + } + ServerEvent::SetCredentials(creds) => { + self.set_credentials(Some(creds)); + } + ev => { + debug!("Unexpected event {:?}", ev); + } + } + }, + Ok((stream, peer)) = listener.accept() => { + debug!(?peer, "Received connection"); + drop(ev_receiver); + if let Err(error) = self.run_connection(stream).await { + error!(?error, "Connection error"); + } + self.static_channels = StaticChannelSet::new(); + } + else => break, + } + } + + Ok(()) + } + + pub fn get_svc_processor(&mut self) -> Option<&mut T> { + self.static_channels + .get_by_type_mut::() + .and_then(|svc| svc.channel_processor_downcast_mut()) + } + + pub fn get_channel_id_by_type(&self) -> Option { + self.static_channels.get_channel_id_by_type::() + } + + async fn dispatch_pdu( + &mut self, + action: Action, + bytes: bytes::BytesMut, + writer: &mut impl FramedWrite, + io_channel_id: u16, + user_channel_id: u16, + ) -> Result { + match action { + Action::FastPath => { + let input = decode(&bytes)?; + self.handle_fastpath(input).await; + } + + Action::X224 => { + if self + .handle_x224(writer, io_channel_id, user_channel_id, &bytes) + .await + .context("X224 input error")? + { + debug!("Got disconnect request"); + return Ok(RunState::Disconnect); + } + } + } + + Ok(RunState::Continue) + } + + async fn dispatch_display_update( + update: DisplayUpdate, + writer: &mut impl FramedWrite, + user_channel_id: u16, + io_channel_id: u16, + buffer: &mut Vec, + mut encoder: UpdateEncoder, + ) -> Result<(RunState, UpdateEncoder)> { + if let DisplayUpdate::Resize(desktop_size) = update { + debug!(?desktop_size, "Display resize"); + encoder.set_desktop_size(desktop_size); + deactivate_all(io_channel_id, user_channel_id, writer).await?; + return Ok((RunState::DeactivationReactivation { desktop_size }, encoder)); + } + + let mut encoder_iter = encoder.update(update); + loop { + let Some(fragmenter) = encoder_iter.next().await else { + break; + }; + + let mut fragmenter = fragmenter.context("error while encoding")?; + if fragmenter.size_hint() > buffer.len() { + buffer.resize(fragmenter.size_hint(), 0); + } + + while let Some(len) = fragmenter.next(buffer) { + writer + .write_all(&buffer[..len]) + .await + .context("failed to write display update")?; + } + } + + Ok((RunState::Continue, encoder)) + } + + async fn dispatch_server_events( + &mut self, + events: &mut Vec, + writer: &mut impl FramedWrite, + user_channel_id: u16, + ) -> Result { + // Avoid wave message queuing up and causing extra delays. + // This is a naive solution, better solutions should compute the actual delay, add IO priority, encode audio, use UDP etc. + // 4 frames should roughly corresponds to hundreds of ms in regular setups. + let mut wave_limit = 4; + for event in events.drain(..) { + trace!(?event, "Dispatching"); + match event { + ServerEvent::Quit(reason) => { + debug!("Got quit event: {reason}"); + return Ok(RunState::Disconnect); + } + ServerEvent::GetLocalAddr(tx) => { + let _ = tx.send(self.local_addr); + } + ServerEvent::SetCredentials(creds) => { + self.set_credentials(Some(creds)); + } + ServerEvent::Rdpsnd(s) => { + let Some(rdpsnd) = self.get_svc_processor::() else { + warn!("No rdpsnd channel, dropping event"); + continue; + }; + let msgs = match s { + RdpsndServerMessage::Wave(data, ts) => { + if wave_limit == 0 { + debug!("Dropping wave"); + continue; + } + wave_limit -= 1; + rdpsnd.wave(data, ts) + } + RdpsndServerMessage::SetVolume { left, right } => rdpsnd.set_volume(left, right), + RdpsndServerMessage::Close => rdpsnd.close(), + RdpsndServerMessage::Error(error) => { + error!(?error, "Handling rdpsnd event"); + continue; + } + } + .context("failed to send rdpsnd event")?; + let channel_id = self + .get_channel_id_by_type::() + .ok_or_else(|| anyhow!("SVC channel not found"))?; + let data = server_encode_svc_messages(msgs.into(), channel_id, user_channel_id)?; + writer.write_all(&data).await?; + } + ServerEvent::Clipboard(c) => { + let Some(cliprdr) = self.get_svc_processor::() else { + warn!("No clipboard channel, dropping event"); + continue; + }; + let msgs = match c { + ClipboardMessage::SendInitiateCopy(formats) => cliprdr.initiate_copy(&formats), + ClipboardMessage::SendFormatData(data) => cliprdr.submit_format_data(data), + ClipboardMessage::SendInitiatePaste(format) => cliprdr.initiate_paste(format), + ClipboardMessage::Error(error) => { + error!(?error, "Handling clipboard event"); + continue; + } + } + .context("failed to send clipboard event")?; + let channel_id = self + .get_channel_id_by_type::() + .ok_or_else(|| anyhow!("SVC channel not found"))?; + let data = server_encode_svc_messages(msgs.into(), channel_id, user_channel_id)?; + writer.write_all(&data).await?; + } + } + } + + Ok(RunState::Continue) + } + + async fn client_loop( + &mut self, + reader: &mut Framed, + writer: &mut Framed, + io_channel_id: u16, + user_channel_id: u16, + mut encoder: UpdateEncoder, + ) -> Result + where + R: FramedRead, + W: FramedWrite, + { + debug!("Starting client loop"); + let mut display_updates = self.display.lock().await.updates().await?; + let mut writer = SharedWriter::new(writer); + let mut display_writer = writer.clone(); + let mut event_writer = writer.clone(); + let ev_receiver = Arc::clone(&self.ev_receiver); + let s = Rc::new(Mutex::new(self)); + + let this = Rc::clone(&s); + let dispatch_pdu = async move { + loop { + let (action, bytes) = reader.read_pdu().await?; + let mut this = this.lock().await; + match this + .dispatch_pdu(action, bytes, &mut writer, io_channel_id, user_channel_id) + .await? + { + RunState::Continue => continue, + state => break Ok(state), + } + } + }; + + let dispatch_display = async move { + let mut buffer = vec![0u8; 4096]; + + loop { + match display_updates.next_update().await { + Ok(Some(update)) => { + match Self::dispatch_display_update( + update, + &mut display_writer, + user_channel_id, + io_channel_id, + &mut buffer, + encoder, + ) + .await? + { + (RunState::Continue, enc) => { + encoder = enc; + continue; + } + (state, _) => { + break Ok(state); + } + } + } + Ok(None) => { + break Ok(RunState::Disconnect); + } + Err(error) => { + warn!(error = format!("{error:#}"), "next_updated failed"); + } + } + } + }; + + let this = Rc::clone(&s); + let mut ev_receiver = ev_receiver.lock().await; + let dispatch_events = async move { + let mut events = Vec::with_capacity(100); + loop { + let nevents = ev_receiver.recv_many(&mut events, 100).await; + if nevents == 0 { + debug!("No sever events.. stopping"); + break Ok(RunState::Disconnect); + } + while let Ok(ev) = ev_receiver.try_recv() { + events.push(ev); + } + let mut this = this.lock().await; + match this + .dispatch_server_events(&mut events, &mut event_writer, user_channel_id) + .await? + { + RunState::Continue => continue, + state => break Ok(state), + } + } + }; + + let state = tokio::select!( + state = dispatch_pdu => state, + state = dispatch_display => state, + state = dispatch_events => state, + ); + + debug!("End of client loop: {state:?}"); + state + } + + async fn client_accepted( + &mut self, + reader: &mut Framed, + writer: &mut Framed, + result: AcceptorResult, + ) -> Result + where + R: FramedRead, + W: FramedWrite, + { + debug!("Client accepted"); + + if !result.input_events.is_empty() { + debug!("Handling input event backlog from acceptor sequence"); + self.handle_input_backlog( + writer, + result.io_channel_id, + result.user_channel_id, + result.input_events, + ) + .await?; + } + + self.static_channels = result.static_channels; + if !result.reactivation { + for (_type_id, channel, channel_id) in self.static_channels.iter_mut() { + debug!(?channel, ?channel_id, "Start"); + let Some(channel_id) = channel_id else { + continue; + }; + let svc_responses = channel.start()?; + let response = server_encode_svc_messages(svc_responses, channel_id, result.user_channel_id)?; + writer.write_all(&response).await?; + } + } + + let mut update_codecs = UpdateEncoderCodecs::new(); + let mut surface_flags = CmdFlags::empty(); + for c in result.capabilities { + match c { + CapabilitySet::General(c) => { + let fastpath = c.extra_flags.contains(GeneralExtraFlags::FASTPATH_OUTPUT_SUPPORTED); + if !fastpath { + bail!("Fastpath output not supported!"); + } + } + CapabilitySet::Bitmap(b) => { + if !b.desktop_resize_flag { + debug!("Desktop resize is not supported by the client"); + continue; + } + + let client_size = DesktopSize { + width: b.desktop_width, + height: b.desktop_height, + }; + let display_size = self.display.lock().await.size().await; + + // It's problematic when the client didn't resize, as we send bitmap updates that don't fit. + // The client will likely drop the connection. + if client_size.width < display_size.width || client_size.height < display_size.height { + // TODO: we may have different behaviour instead, such as clipping or scaling? + warn!( + "Client size doesn't fit the server size: {:?} < {:?}", + client_size, display_size + ); + } + } + CapabilitySet::SurfaceCommands(c) => { + surface_flags = c.flags; + } + CapabilitySet::BitmapCodecs(BitmapCodecs(codecs)) => { + for codec in codecs { + match codec.property { + // FIXME: The encoder operates in image mode only. + // + // See [MS-RDPRFX] 3.1.1.1 "State Machine" for + // implementation of the video mode. which allows to + // skip sending Header for each image. + // + // We should distinguish parameters for both modes, + // and somehow choose the "best", instead of picking + // the last parsed here. + CodecProperty::RemoteFx(rdp::capability_sets::RemoteFxContainer::ClientContainer(c)) + if self.opts.has_remote_fx() => + { + for caps in c.caps_data.0 .0 { + update_codecs.set_remotefx(Some((caps.entropy_bits, codec.id))); + } + } + CodecProperty::ImageRemoteFx(rdp::capability_sets::RemoteFxContainer::ClientContainer( + c, + )) if self.opts.has_image_remote_fx() => { + for caps in c.caps_data.0 .0 { + update_codecs.set_remotefx(Some((caps.entropy_bits, codec.id))); + } + } + CodecProperty::NsCodec(_) => (), + #[cfg(feature = "qoi")] + CodecProperty::Qoi if self.opts.has_qoi() => { + update_codecs.set_qoi(Some(codec.id)); + } + #[cfg(feature = "qoiz")] + CodecProperty::QoiZ if self.opts.has_qoiz() => { + update_codecs.set_qoiz(Some(codec.id)); + } + _ => (), + } + } + } + _ => {} + } + } + + let desktop_size = self.display.lock().await.size().await; + let encoder = UpdateEncoder::new(desktop_size, surface_flags, update_codecs) + .context("failed to initialize update encoder")?; + + let state = self + .client_loop(reader, writer, result.io_channel_id, result.user_channel_id, encoder) + .await + .context("client loop failure")?; + + Ok(state) + } + + async fn handle_input_backlog( + &mut self, + writer: &mut impl FramedWrite, + io_channel_id: u16, + user_channel_id: u16, + frames: Vec>, + ) -> Result<()> { + for frame in frames { + match Action::from_fp_output_header(frame[0]) { + Ok(Action::FastPath) => { + let input = decode(&frame)?; + self.handle_fastpath(input).await; + } + + Ok(Action::X224) => { + let _ = self.handle_x224(writer, io_channel_id, user_channel_id, &frame).await; + } + + // the frame here is always valid, because otherwise it would + // have failed during the acceptor loop + Err(_) => unreachable!(), + } + } + + Ok(()) + } + + async fn handle_fastpath(&mut self, input: FastPathInput) { + for event in input.input_events().iter().copied() { + let mut handler = self.handler.lock().await; + match event { + FastPathInputEvent::KeyboardEvent(flags, key) => { + handler.keyboard((key, flags).into()); + } + + FastPathInputEvent::UnicodeKeyboardEvent(flags, key) => { + handler.keyboard((key, flags).into()); + } + + FastPathInputEvent::SyncEvent(flags) => { + handler.keyboard(flags.into()); + } + + FastPathInputEvent::MouseEvent(mouse) => { + handler.mouse(mouse.into()); + } + + FastPathInputEvent::MouseEventEx(mouse) => { + handler.mouse(mouse.into()); + } + + FastPathInputEvent::MouseEventRel(mouse) => { + handler.mouse(mouse.into()); + } + + FastPathInputEvent::QoeEvent(quality) => { + warn!("Received QoE: {}", quality); + } + } + } + } + + async fn handle_io_channel_data(&mut self, data: SendDataRequest<'_>) -> Result { + let control: rdp::headers::ShareControlHeader = decode(data.user_data.as_ref())?; + + match control.share_control_pdu { + ShareControlPdu::Data(header) => match header.share_data_pdu { + rdp::headers::ShareDataPdu::Input(pdu) => { + self.handle_input_event(pdu).await; + } + + rdp::headers::ShareDataPdu::ShutdownRequest => { + return Ok(true); + } + + unexpected => { + warn!(?unexpected, "Unexpected share data pdu"); + } + }, + + unexpected => { + warn!(?unexpected, "Unexpected share control"); + } + } + + Ok(false) + } + + async fn handle_x224( + &mut self, + writer: &mut impl FramedWrite, + io_channel_id: u16, + user_channel_id: u16, + frame: &[u8], + ) -> Result { + let message = decode::>>(frame)?; + match message.0 { + mcs::McsMessage::SendDataRequest(data) => { + debug!(?data, "McsMessage::SendDataRequest"); + if data.channel_id == io_channel_id { + return self.handle_io_channel_data(data).await; + } + + if let Some(svc) = self.static_channels.get_by_channel_id_mut(data.channel_id) { + let response_pdus = svc.process(&data.user_data)?; + let response = server_encode_svc_messages(response_pdus, data.channel_id, user_channel_id)?; + writer.write_all(&response).await?; + } else { + warn!(channel_id = data.channel_id, "Unexpected channel received: ID",); + } + } + + mcs::McsMessage::DisconnectProviderUltimatum(disconnect) => { + if disconnect.reason == mcs::DisconnectReason::UserRequested { + return Ok(true); + } + } + + _ => { + warn!(name = ironrdp_core::name(&message), "Unexpected mcs message"); + } + } + + Ok(false) + } + + async fn handle_input_event(&mut self, input: InputEventPdu) { + for event in input.0 { + let mut handler = self.handler.lock().await; + match event { + ironrdp_pdu::input::InputEvent::ScanCode(key) => { + handler.keyboard((key.key_code, key.flags).into()); + } + + ironrdp_pdu::input::InputEvent::Unicode(key) => { + handler.keyboard((key.unicode_code, key.flags).into()); + } + + ironrdp_pdu::input::InputEvent::Sync(sync) => { + handler.keyboard(sync.flags.into()); + } + + ironrdp_pdu::input::InputEvent::Mouse(mouse) => { + handler.mouse(mouse.into()); + } + + ironrdp_pdu::input::InputEvent::MouseX(mouse) => { + handler.mouse(mouse.into()); + } + + ironrdp_pdu::input::InputEvent::MouseRel(mouse) => { + handler.mouse(mouse.into()); + } + + ironrdp_pdu::input::InputEvent::Unused(_) => {} + } + } + } + + async fn accept_finalize(&mut self, mut framed: TokioFramed, mut acceptor: Acceptor) -> Result> + where + S: AsyncRead + AsyncWrite + Sync + Send + Unpin, + { + loop { + let (new_framed, result) = ironrdp_acceptor::accept_finalize(framed, &mut acceptor) + .await + .context("failed to accept client during finalize")?; + + let (mut reader, mut writer) = split_tokio_framed(new_framed); + + match self.client_accepted(&mut reader, &mut writer, result).await? { + RunState::Continue => { + unreachable!(); + } + RunState::DeactivationReactivation { desktop_size } => { + // No description of such behavior was found in the + // specification, but apparently, we must keep the channel + // state as they were during reactivation. This fixes + // various state issues during client resize. + acceptor = Acceptor::new_deactivation_reactivation( + acceptor, + core::mem::take(&mut self.static_channels), + desktop_size, + )?; + framed = unsplit_tokio_framed(reader, writer); + continue; + } + RunState::Disconnect => { + let final_framed = unsplit_tokio_framed(reader, writer); + return Ok(final_framed); + } + } + } + } + + pub fn set_credentials(&mut self, creds: Option) { + debug!(?creds, "Changing credentials"); + self.creds = creds + } +} + +async fn deactivate_all( + io_channel_id: u16, + user_channel_id: u16, + writer: &mut impl FramedWrite, +) -> Result<(), anyhow::Error> { + let pdu = ShareControlPdu::ServerDeactivateAll(ServerDeactivateAll); + let pdu = rdp::headers::ShareControlHeader { + share_id: 0, + pdu_source: io_channel_id, + share_control_pdu: pdu, + }; + let user_data = encode_vec(&pdu)?.into(); + let pdu = SendDataIndication { + initiator_id: user_channel_id, + channel_id: io_channel_id, + user_data, + }; + let msg = encode_vec(&X224(pdu))?; + writer.write_all(&msg).await?; + Ok(()) +} + +struct SharedWriter<'w, W: FramedWrite> { + writer: Rc>, +} + +impl Clone for SharedWriter<'_, W> { + fn clone(&self) -> Self { + Self { + writer: Rc::clone(&self.writer), + } + } +} + +impl FramedWrite for SharedWriter<'_, W> +where + W: FramedWrite, +{ + type WriteAllFut<'write> + = core::pin::Pin> + 'write>> + where + Self: 'write; + + fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a> { + Box::pin(async { + let mut writer = self.writer.lock().await; + + writer.write_all(buf).await?; + Ok(()) + }) + } +} + +impl<'a, W: FramedWrite> SharedWriter<'a, W> { + fn new(writer: &'a mut W) -> Self { + Self { + writer: Rc::new(Mutex::new(writer)), + } + } +} diff --git a/crates/ironrdp-server/src/sound.rs b/crates/ironrdp-server/src/sound.rs new file mode 100644 index 00000000..acd109d3 --- /dev/null +++ b/crates/ironrdp-server/src/sound.rs @@ -0,0 +1,7 @@ +pub use ironrdp_rdpsnd::server::{RdpsndServerHandler, RdpsndServerMessage}; + +use crate::ServerEventSender; + +pub trait SoundServerFactory: ServerEventSender { + fn build_backend(&self) -> Box; +} diff --git a/crates/ironrdp-session-generators/Cargo.toml b/crates/ironrdp-session-generators/Cargo.toml new file mode 100644 index 00000000..8d140e65 --- /dev/null +++ b/crates/ironrdp-session-generators/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ironrdp-session-generators" +version = "0.0.0" +description = "`proptest` generators for `ironrdp-session` types" +publish = false +edition.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +# ironrdp-session.workspace = true +# ironrdp-pdu-generators.workspace = true +# proptest.workspace = true + +[lints] +workspace = true + diff --git a/crates/ironrdp-session-generators/README.md b/crates/ironrdp-session-generators/README.md new file mode 100644 index 00000000..25623639 --- /dev/null +++ b/crates/ironrdp-session-generators/README.md @@ -0,0 +1,7 @@ +# IronRDP session generators + +`proptest` generators for `ironrdp-session` types. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-session-generators/src/lib.rs b/crates/ironrdp-session-generators/src/lib.rs new file mode 100644 index 00000000..df55d21d --- /dev/null +++ b/crates/ironrdp-session-generators/src/lib.rs @@ -0,0 +1,8 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +// No need to be as strict as in production libraries +#![allow(clippy::arithmetic_side_effects)] +#![allow(clippy::cast_lossless)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_possible_wrap)] +#![allow(clippy::cast_sign_loss)] diff --git a/crates/ironrdp-session/CHANGELOG.md b/crates/ironrdp-session/CHANGELOG.md new file mode 100644 index 00000000..90011784 --- /dev/null +++ b/crates/ironrdp-session/CHANGELOG.md @@ -0,0 +1,83 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.7.0...ironrdp-session-v0.8.0)] - 2025-12-18 + + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.5.0...ironrdp-session-v0.6.0)] - 2025-08-29 + +### Features + +- Add QOI image codec ([613fd51f26](https://github.com/Devolutions/IronRDP/commit/613fd51f26315d8212662c46f8e625c541e4bb59)) + + The Quite OK Image format ([1]) losslessly compresses images to a similar size + of PNG, while offering 20x-50x faster encoding and 3x-4x faster decoding. + +- Add QOIZ image codec ([87df67fdc7](https://github.com/Devolutions/IronRDP/commit/87df67fdc76ff4f39d4b83521e34bf3b5e2e73bb)) + + Add a new QOIZ codec for SetSurface command. The PDU data contains the same + data as the QOI codec, with zstd compression. + +## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.4.0...ironrdp-session-v0.4.1)] - 2025-06-27 + +### Features + +- More functions on `ActiveStage` (#791) ([5482365655](https://github.com/Devolutions/IronRDP/commit/5482365655e5c171cd967eda401b01161a9f6602)) + - `get_dvc_by_channel_id` + - `encode_dvc_messages` + + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.3.0...ironrdp-session-v0.4.0)] - 2025-05-27 + +### Features + +- [**breaking**] Make DecodedImage Send ([45f66117ba](https://github.com/Devolutions/IronRDP/commit/45f66117ba05170d95b21ec7d97017b44b954f28)) + +- Add DecodeImage helpers ([cd7a60ba45](https://github.com/Devolutions/IronRDP/commit/cd7a60ba45a0241be4ecf3860ec4f82b431a7ce2)) + +### Bug Fixes + +- Update rectangle when applying None codecs updates (#728) ([a50cd643dc](https://github.com/Devolutions/IronRDP/commit/a50cd643dce9621f314231b7598d2fd31e4718c6)) + +- Return the correct updated region ([7507a152f1](https://github.com/Devolutions/IronRDP/commit/7507a152f14db594e4067bbc01e243cfba77770f)) + + "update_rectangle" is set to empty(). The surface updates are then added + by "union". But a union with an empty rectangle at (0,0) is still a + rectangle at (0,0). We end up with big region updates rooted at (0,0)... + +- Decrease verbosity of Rfx frame_index ([b31b99eafb](https://github.com/Devolutions/IronRDP/commit/b31b99eafb0aac2a5e5a610af21a4027ae5cd698)) + +- Decrease verbosity of FastPath header ([f9b6992e74](https://github.com/Devolutions/IronRDP/commit/f9b6992e74abb929f3001e76abaff5d7215e1cb4)) + + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.3...ironrdp-session-v0.3.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.2.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.2...ironrdp-session-v0.2.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + + +## [[0.2.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.1...ironrdp-session-v0.2.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.0...ironrdp-session-v0.2.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-session/Cargo.toml b/crates/ironrdp-session/Cargo.toml new file mode 100644 index 00000000..dd00422d --- /dev/null +++ b/crates/ironrdp-session/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "ironrdp-session" +version = "0.8.0" +readme = "README.md" +description = "State machines to drive an RDP session" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"] +qoiz = ["dep:zstd-safe", "qoi"] + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.8" } # public # TODO: at some point, this dependency could be removed (good for compilation speed) +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.4" } # public +ironrdp-error = { path = "../ironrdp-error", version = "0.1" } # public +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.7" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6", features = ["std"] } # public +ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.4" } +tracing = { version = "0.1", features = ["log"] } +qoicoubeh = { version = "0.5", optional = true } +zstd-safe = { version = "7.2", optional = true, features = ["std"] } + +[lints] +workspace = true diff --git a/crates/ironrdp-session/LICENSE-APACHE b/crates/ironrdp-session/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-session/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-session/LICENSE-MIT b/crates/ironrdp-session/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-session/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-session/README.md b/crates/ironrdp-session/README.md new file mode 100644 index 00000000..dfaa1370 --- /dev/null +++ b/crates/ironrdp-session/README.md @@ -0,0 +1,7 @@ +# IronRDP Session + +Abstract state machine to drive an RDP session. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-session/src/active_stage.rs b/crates/ironrdp-session/src/active_stage.rs new file mode 100644 index 00000000..75ef5499 --- /dev/null +++ b/crates/ironrdp-session/src/active_stage.rs @@ -0,0 +1,316 @@ +use std::sync::Arc; + +use ironrdp_connector::connection_activation::ConnectionActivationSequence; +use ironrdp_connector::ConnectionResult; +use ironrdp_core::WriteBuf; +use ironrdp_displaycontrol::client::DisplayControlClient; +use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel}; +use ironrdp_graphics::pointer::DecodedPointer; +use ironrdp_pdu::geometry::InclusiveRectangle; +use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent}; +use ironrdp_pdu::rdp::headers::ShareDataPdu; +use ironrdp_pdu::{mcs, Action}; +use ironrdp_svc::{SvcMessage, SvcProcessor, SvcProcessorMessages}; +use tracing::debug; + +use crate::fast_path::UpdateKind; +use crate::image::DecodedImage; +use crate::{fast_path, x224, SessionError, SessionErrorExt as _, SessionResult}; + +pub struct ActiveStage { + x224_processor: x224::Processor, + fast_path_processor: fast_path::Processor, + enable_server_pointer: bool, +} + +impl ActiveStage { + pub fn new(connection_result: ConnectionResult) -> Self { + let x224_processor = x224::Processor::new( + connection_result.static_channels, + connection_result.user_channel_id, + connection_result.io_channel_id, + connection_result.connection_activation, + ); + + let fast_path_processor = fast_path::ProcessorBuilder { + io_channel_id: connection_result.io_channel_id, + user_channel_id: connection_result.user_channel_id, + enable_server_pointer: connection_result.enable_server_pointer, + pointer_software_rendering: connection_result.pointer_software_rendering, + } + .build(); + + Self { + x224_processor, + fast_path_processor, + enable_server_pointer: connection_result.enable_server_pointer, + } + } + + pub fn update_mouse_pos(&mut self, x: u16, y: u16) { + self.fast_path_processor.update_mouse_pos(x, y); + } + + /// Encodes outgoing input events and modifies image if necessary (e.g for client-side pointer + /// rendering). + pub fn process_fastpath_input( + &mut self, + image: &mut DecodedImage, + events: &[FastPathInputEvent], + ) -> SessionResult> { + if events.is_empty() { + return Ok(Vec::new()); + } + + // Mouse move events are prevalent, so we can preallocate space for + // response frame + graphics update + let mut output = Vec::with_capacity(2); + + // Encoding fastpath response frame + // PERF: unnecessary copy + let fastpath_input = FastPathInput::new(events.to_vec()).map_err(SessionError::decode)?; + let frame = ironrdp_core::encode_vec(&fastpath_input).map_err(SessionError::encode)?; + output.push(ActiveStageOutput::ResponseFrame(frame)); + + // If pointer rendering is disabled - we can skip the rest + if !self.enable_server_pointer { + return Ok(output); + } + + // If mouse was moved by client - we should update framebuffer to reflect new + // pointer position + let mouse_pos = events.iter().find_map(|event| match event { + FastPathInputEvent::MouseEvent(event) => Some((event.x_position, event.y_position)), + FastPathInputEvent::MouseEventEx(event) => Some((event.x_position, event.y_position)), + _ => None, + }); + + let (mouse_x, mouse_y) = match mouse_pos { + Some(mouse_pos) => mouse_pos, + None => return Ok(output), + }; + + // Graphics update is only sent when update is visually changed the framebuffer + if let Some(rect) = image.move_pointer(mouse_x, mouse_y)? { + output.push(ActiveStageOutput::GraphicsUpdate(rect)); + } + + Ok(output) + } + + /// Process a frame received from the server. + pub fn process( + &mut self, + image: &mut DecodedImage, + action: Action, + frame: &[u8], + ) -> SessionResult> { + let (mut stage_outputs, processor_updates) = match action { + Action::FastPath => { + let mut output = WriteBuf::new(); + let processor_updates = self.fast_path_processor.process(image, frame, &mut output)?; + ( + vec![ActiveStageOutput::ResponseFrame(output.into_inner())], + processor_updates, + ) + } + Action::X224 => { + let outputs = self + .x224_processor + .process(frame)? + .into_iter() + .map(TryFrom::try_from) + .collect::, _>>()?; + (outputs, Vec::new()) + } + }; + + for update in processor_updates { + match update { + UpdateKind::None => {} + UpdateKind::Region(region) => { + stage_outputs.push(ActiveStageOutput::GraphicsUpdate(region)); + } + UpdateKind::PointerDefault => { + stage_outputs.push(ActiveStageOutput::PointerDefault); + } + UpdateKind::PointerHidden => { + stage_outputs.push(ActiveStageOutput::PointerHidden); + } + UpdateKind::PointerPosition { x, y } => { + stage_outputs.push(ActiveStageOutput::PointerPosition { x, y }); + } + UpdateKind::PointerBitmap(pointer) => { + stage_outputs.push(ActiveStageOutput::PointerBitmap(pointer)); + } + } + } + + Ok(stage_outputs) + } + + pub fn set_fastpath_processor(&mut self, processor: fast_path::Processor) { + self.fast_path_processor = processor; + } + + pub fn set_enable_server_pointer(&mut self, enable_server_pointer: bool) { + self.enable_server_pointer = enable_server_pointer; + } + + /// Encodes client-side graceful shutdown request. Note that upon sending this request, + /// client should wait for server's ShutdownDenied PDU before closing the connection. + /// + /// Client-side graceful shutdown is defined in [MS-RDPBCGR] + /// + /// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68 + pub fn graceful_shutdown(&self) -> SessionResult> { + let mut frame = WriteBuf::new(); + self.x224_processor + .encode_static(&mut frame, ShareDataPdu::ShutdownRequest)?; + + Ok(vec![ActiveStageOutput::ResponseFrame(frame.into_inner())]) + } + + /// Send a pdu on the static global channel. Typically used to send input events + pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult { + self.x224_processor.encode_static(output, pdu) + } + + pub fn get_svc_processor(&mut self) -> Option<&T> { + self.x224_processor.get_svc_processor() + } + + pub fn get_svc_processor_mut(&mut self) -> Option<&mut T> { + self.x224_processor.get_svc_processor_mut() + } + + pub fn get_dvc(&mut self) -> Option<&DynamicVirtualChannel> { + self.x224_processor.get_dvc::() + } + + pub fn get_dvc_by_channel_id(&mut self, channel_id: u32) -> Option<&DynamicVirtualChannel> { + self.x224_processor.get_dvc_by_channel_id(channel_id) + } + + /// Completes user's SVC request with data, required to sent it over the network and returns + /// a buffer with encoded data. + pub fn process_svc_processor_messages( + &self, + messages: SvcProcessorMessages, + ) -> SessionResult> { + self.x224_processor.process_svc_processor_messages(messages) + } + + /// Fully encodes a resize request for sending over the Display Control Virtual Channel. + /// + /// If the Display Control Virtual Channel is not available, or not yet connected, this method + /// will return `None`. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent. + /// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm. + /// + /// Use [`ironrdp_displaycontrol::pdu::MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function + /// to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn encode_resize( + &mut self, + width: u32, + height: u32, + scale_factor: Option, + physical_dims: Option<(u32, u32)>, + ) -> Option>> { + if let Some(dvc) = self.get_dvc::() { + if let Some(channel_id) = dvc.channel_id() { + let display_control = dvc.channel_processor_downcast_ref::()?; + let svc_messages = match display_control.encode_single_primary_monitor( + channel_id, + width, + height, + scale_factor, + physical_dims, + ) { + Ok(messages) => messages, + Err(e) => return Some(Err(SessionError::encode(e))), + }; + + return Some( + self.process_svc_processor_messages(SvcProcessorMessages::::new(svc_messages)), + ); + } else { + debug!("Could not encode a resize: Display Control Virtual Channel is not yet connected"); + } + } else { + debug!("Could not encode a resize: Display Control Virtual Channel is not available"); + } + + None + } + + pub fn encode_dvc_messages(&mut self, messages: Vec) -> SessionResult> { + self.process_svc_processor_messages(SvcProcessorMessages::::new(messages)) + } +} + +#[derive(Debug)] +pub enum ActiveStageOutput { + ResponseFrame(Vec), + GraphicsUpdate(InclusiveRectangle), + PointerDefault, + PointerHidden, + PointerPosition { x: u16, y: u16 }, + PointerBitmap(Arc), + Terminate(GracefulDisconnectReason), + DeactivateAll(Box), +} + +impl TryFrom for ActiveStageOutput { + type Error = SessionError; + + fn try_from(value: x224::ProcessorOutput) -> Result { + match value { + x224::ProcessorOutput::ResponseFrame(frame) => Ok(Self::ResponseFrame(frame)), + x224::ProcessorOutput::Disconnect(desc) => { + let desc = match desc { + x224::DisconnectDescription::McsDisconnect(reason) => match reason { + mcs::DisconnectReason::ProviderInitiated => GracefulDisconnectReason::ServerInitiated, + mcs::DisconnectReason::UserRequested => GracefulDisconnectReason::UserInitiated, + other => GracefulDisconnectReason::Other(other.description().to_owned()), + }, + x224::DisconnectDescription::ErrorInfo(info) => GracefulDisconnectReason::Other(info.description()), + }; + + Ok(Self::Terminate(desc)) + } + x224::ProcessorOutput::DeactivateAll(cas) => Ok(Self::DeactivateAll(cas)), + } + } +} + +/// Reasons for graceful disconnect. This type provides GUI-friendly descriptions for +/// disconnect reasons. +#[derive(Debug, Clone)] +pub enum GracefulDisconnectReason { + UserInitiated, + ServerInitiated, + Other(String), +} + +impl GracefulDisconnectReason { + pub fn description(&self) -> String { + match self { + GracefulDisconnectReason::UserInitiated => "user initiated disconnect".to_owned(), + GracefulDisconnectReason::ServerInitiated => "server initiated disconnect".to_owned(), + GracefulDisconnectReason::Other(description) => description.clone(), + } + } +} + +impl core::fmt::Display for GracefulDisconnectReason { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(&self.description()) + } +} diff --git a/crates/ironrdp-session/src/fast_path.rs b/crates/ironrdp-session/src/fast_path.rs new file mode 100644 index 00000000..9826fbf9 --- /dev/null +++ b/crates/ironrdp-session/src/fast_path.rs @@ -0,0 +1,559 @@ +use std::sync::Arc; + +use ironrdp_core::{decode_cursor, DecodeErrorKind, ReadCursor, WriteBuf}; +use ironrdp_graphics::image_processing::PixelFormat; +use ironrdp_graphics::pointer::{DecodedPointer, PointerBitmapTarget}; +use ironrdp_graphics::rdp6::BitmapStreamDecoder; +use ironrdp_graphics::rle::RlePixelFormat; +use ironrdp_pdu::codecs::rfx::FrameAcknowledgePdu; +use ironrdp_pdu::fast_path::{FastPathHeader, FastPathUpdate, FastPathUpdatePdu, Fragmentation}; +use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; +use ironrdp_pdu::pointer::PointerUpdateData; +use ironrdp_pdu::rdp::capability_sets::{CodecId, CODEC_ID_NONE, CODEC_ID_REMOTEFX}; +use ironrdp_pdu::rdp::headers::ShareDataPdu; +use ironrdp_pdu::surface_commands::{FrameAction, FrameMarkerPdu, SurfaceCommand}; +use tracing::{debug, trace, warn}; + +use crate::image::DecodedImage; +use crate::pointer::PointerCache; +use crate::{custom_err, reason_err, rfx, SessionError, SessionErrorExt as _, SessionResult}; + +#[derive(Debug)] +pub enum UpdateKind { + None, + Region(InclusiveRectangle), + PointerDefault, + PointerHidden, + PointerPosition { x: u16, y: u16 }, + PointerBitmap(Arc), +} + +pub struct Processor { + complete_data: CompleteData, + rfx_handler: rfx::DecodingContext, + marker_processor: FrameMarkerProcessor, + bitmap_stream_decoder: BitmapStreamDecoder, + pointer_cache: PointerCache, + use_system_pointer: bool, + mouse_pos_update: Option<(u16, u16)>, + enable_server_pointer: bool, + pointer_software_rendering: bool, + #[cfg(feature = "qoiz")] + zdctx: zstd_safe::DCtx<'static>, +} + +impl Processor { + pub fn update_mouse_pos(&mut self, x: u16, y: u16) { + self.mouse_pos_update = Some((x, y)); + } + + /// Process input fast path frame and return list of updates. + pub fn process( + &mut self, + image: &mut DecodedImage, + input: &[u8], + output: &mut WriteBuf, + ) -> SessionResult> { + let mut processor_updates = Vec::new(); + + if let Some((x, y)) = self.mouse_pos_update.take() { + if let Some(rect) = image.move_pointer(x, y)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + + let mut input = ReadCursor::new(input); + + let header = decode_cursor::(&mut input).map_err(SessionError::decode)?; + trace!(fast_path_header = ?header, "Received Fast-Path packet"); + + let update_pdu = decode_cursor::>(&mut input).map_err(SessionError::decode)?; + trace!(fast_path_update_fragmentation = ?update_pdu.fragmentation); + + let processed_complete_data = self + .complete_data + .process_data(update_pdu.data, update_pdu.fragmentation); + + let update_code = update_pdu.update_code; + + let Some(data) = processed_complete_data else { + return Ok(Vec::new()); + }; + + let update = FastPathUpdate::decode_with_code(data.as_slice(), update_code); + + match update { + Ok(FastPathUpdate::SurfaceCommands(surface_commands)) => { + trace!("Received Surface Commands: {} pieces", surface_commands.len()); + let update_region = self.process_surface_commands(image, output, surface_commands)?; + processor_updates.push(UpdateKind::Region(update_region)); + } + Ok(FastPathUpdate::Bitmap(bitmap_update)) => { + trace!("Received bitmap update"); + + let mut buf = Vec::new(); + let mut update_kind = UpdateKind::None; + + for update in bitmap_update.rectangles { + trace!("{update:?}"); + buf.clear(); + + // Bitmap data is either compressed or uncompressed, depending + // on whether the BITMAP_COMPRESSION flag is present in the + // flags field. + let update_rectangle = if update + .compression_flags + .contains(ironrdp_pdu::bitmap::Compression::BITMAP_COMPRESSION) + { + if update.bits_per_pixel == 32 { + // Compressed bitmaps at a color depth of 32 bpp are compressed using RDP 6.0 + // Bitmap Compression and stored inside an RDP 6.0 Bitmap Compressed Stream + // structure ([MS-RDPEGDI] section 2.2.2.5.1). + debug!("32 bpp compressed RDP6_BITMAP_STREAM"); + + match self.bitmap_stream_decoder.decode_bitmap_stream_to_rgb24( + update.bitmap_data, + &mut buf, + usize::from(update.width), + usize::from(update.height), + ) { + Ok(()) => image.apply_rgb24(&buf, &update.rectangle, true)?, + Err(err) => { + warn!("Invalid RDP6_BITMAP_STREAM: {err}"); + update.rectangle.clone() + } + } + } else { + // Compressed bitmaps not in 32 bpp format are compressed using Interleaved + // RLE and encapsulated in an RLE Compressed Bitmap Stream structure (section + // 2.2.9.1.1.3.1.2.4). + debug!(bpp = update.bits_per_pixel, "Non-32 bpp compressed RLE_BITMAP_STREAM",); + + match ironrdp_graphics::rle::decompress( + update.bitmap_data, + &mut buf, + usize::from(update.width), + usize::from(update.height), + usize::from(update.bits_per_pixel), + ) { + Ok(RlePixelFormat::Rgb16) => image.apply_rgb16_bitmap(&buf, &update.rectangle)?, + + // TODO: support other pixel formats… + Ok(format @ (RlePixelFormat::Rgb8 | RlePixelFormat::Rgb15 | RlePixelFormat::Rgb24)) => { + warn!("Received RLE-compressed bitmap with unsupported color depth: {format:?}"); + update.rectangle.clone() + } + + Err(e) => { + warn!("Invalid RLE-compressed bitmap: {e}"); + update.rectangle.clone() + } + } + } + } else { + // Uncompressed bitmap data is formatted as a bottom-up, left-to-right series of + // pixels. Each pixel is a whole number of bytes. Each row contains a multiple of + // four bytes (including up to three bytes of padding, as necessary). + trace!("Uncompressed raw bitmap"); + + match update.bits_per_pixel { + 16 => image.apply_rgb16_bitmap(update.bitmap_data, &update.rectangle)?, + // TODO: support other pixel formats… + unsupported => { + warn!("Invalid raw bitmap with {unsupported} bytes per pixels"); + update.rectangle.clone() + } + } + }; + + match update_kind { + UpdateKind::Region(current) => { + update_kind = UpdateKind::Region(current.union(&update_rectangle)) + } + _ => update_kind = UpdateKind::Region(update_rectangle), + } + } + + processor_updates.push(update_kind); + } + Ok(FastPathUpdate::Pointer(update)) => { + if !self.enable_server_pointer { + return Ok(processor_updates); + } + + let bitmap_target = if self.pointer_software_rendering { + PointerBitmapTarget::Software + } else { + PointerBitmapTarget::Accelerated + }; + + match update { + PointerUpdateData::SetHidden => { + processor_updates.push(UpdateKind::PointerHidden); + if self.pointer_software_rendering && !self.use_system_pointer { + self.use_system_pointer = true; + if let Some(rect) = image.hide_pointer()? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + } + PointerUpdateData::SetDefault => { + processor_updates.push(UpdateKind::PointerDefault); + if self.pointer_software_rendering && !self.use_system_pointer { + self.use_system_pointer = true; + if let Some(rect) = image.hide_pointer()? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + } + PointerUpdateData::SetPosition(position) => { + if self.use_system_pointer || !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerPosition { + x: position.x, + y: position.y, + }); + } else if let Some(rect) = image.move_pointer(position.x, position.y)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + PointerUpdateData::Color(pointer) => { + let cache_index = pointer.cache_index; + + let decoded_pointer = Arc::new( + DecodedPointer::decode_color_pointer_attribute(&pointer, bitmap_target) + .map_err(|e| SessionError::custom("failed to decode color pointer attribute", e))?, + ); + + let _ = self + .pointer_cache + .insert(usize::from(cache_index), Arc::clone(&decoded_pointer)); + + if !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer))); + } else if let Some(rect) = image.update_pointer(decoded_pointer)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + PointerUpdateData::Cached(cached) => { + let cache_index = cached.cache_index; + + if let Some(cached_pointer) = self.pointer_cache.get(usize::from(cache_index)) { + // Disable system pointer + processor_updates.push(UpdateKind::PointerHidden); + self.use_system_pointer = false; + // Send graphics update + if !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&cached_pointer))); + } else if let Some(rect) = image.update_pointer(cached_pointer)? { + processor_updates.push(UpdateKind::Region(rect)); + } else { + // In case pointer was hidden previously + if let Some(rect) = image.show_pointer()? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + } else { + warn!("Cached pointer not found {}", cache_index); + } + } + PointerUpdateData::New(pointer) => { + let cache_index = pointer.color_pointer.cache_index; + + let decoded_pointer = Arc::new( + DecodedPointer::decode_pointer_attribute(&pointer, bitmap_target) + .map_err(|e| SessionError::custom("failed to decode pointer attribute", e))?, + ); + + let _ = self + .pointer_cache + .insert(usize::from(cache_index), Arc::clone(&decoded_pointer)); + + if !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer))); + } else if let Some(rect) = image.update_pointer(decoded_pointer)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + PointerUpdateData::Large(pointer) => { + let cache_index = pointer.cache_index; + + let decoded_pointer: Arc = Arc::new( + DecodedPointer::decode_large_pointer_attribute(&pointer, bitmap_target) + .map_err(|e| SessionError::custom("failed to decode large pointer attribute", e))?, + ); + + let _ = self + .pointer_cache + .insert(usize::from(cache_index), Arc::clone(&decoded_pointer)); + + if !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer))); + } else if let Some(rect) = image.update_pointer(decoded_pointer)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + }; + } + Err(e) => { + // FIXME: This seems to be a way of special-handling the error case in FastPathUpdate::decode_cursor_with_code + // to ignore the unsupported update PDUs, but this is a fragile logic and the rationale behind it is not + // obvious. + if let DecodeErrorKind::InvalidField { field, reason } = e.kind() { + warn!(field, reason, "Received invalid Fast-Path update"); + processor_updates.push(UpdateKind::None); + } else { + return Err(custom_err!("Fast-Path", e)); + } + } + }; + + Ok(processor_updates) + } + + fn process_surface_commands( + &mut self, + image: &mut DecodedImage, + output: &mut WriteBuf, + surface_commands: Vec>, + ) -> SessionResult { + let mut update_rectangle = None; + + for command in surface_commands { + match command { + SurfaceCommand::SetSurfaceBits(bits) | SurfaceCommand::StreamSurfaceBits(bits) => { + let codec_id = CodecId::from_u8(bits.extended_bitmap_data.codec_id).ok_or_else(|| { + reason_err!( + "Fast-Path", + "unexpected codec ID: {:x}", + bits.extended_bitmap_data.codec_id + ) + })?; + + trace!(?codec_id, "Surface bits"); + + let destination = bits.destination; + // TODO(@pacmancoder): Correct rectangle conversion logic should + // be revisited when `rectangle_processing.rs` from + // `ironrdp-graphics` will be refactored to use generic `Rectangle` + // trait instead of hardcoded `InclusiveRectangle`. + let destination = InclusiveRectangle { + left: destination.left, + top: destination.top, + right: destination.right - 1, + bottom: destination.bottom - 1, + }; + match codec_id { + CODEC_ID_NONE => { + let ext_data = bits.extended_bitmap_data; + match ext_data.bpp { + 32 => { + let rectangle = + image.apply_rgb32_bitmap(ext_data.data, PixelFormat::BgrX32, &destination)?; + update_rectangle = update_rectangle + .map(|rect: InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + } + bpp => { + warn!("Unsupported bpp: {bpp}") + } + } + } + CODEC_ID_REMOTEFX => { + let mut data = ReadCursor::new(bits.extended_bitmap_data.data); + while !data.is_empty() { + let (_frame_id, rectangle) = self.rfx_handler.decode(image, &destination, &mut data)?; + update_rectangle = update_rectangle + .map(|rect: InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + } + } + #[cfg(feature = "qoi")] + ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI => { + qoi_apply( + image, + destination, + bits.extended_bitmap_data.data, + &mut update_rectangle, + )?; + } + #[cfg(feature = "qoiz")] + ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOIZ => { + let compressed = &bits.extended_bitmap_data.data; + let mut input = zstd_safe::InBuffer::around(compressed); + let mut data = vec![0; compressed.len() * 4]; + let mut pos = 0; + loop { + let mut output = zstd_safe::OutBuffer::around_pos(data.as_mut_slice(), pos); + self.zdctx + .decompress_stream(&mut output, &mut input) + .map_err(zstd_safe::get_error_name) + .map_err(|e| reason_err!("zstd", "{}", e))?; + pos = output.pos(); + if pos == output.capacity() { + data.resize(data.capacity() * 2, 0); + } else { + break; + } + } + + qoi_apply(image, destination, &data, &mut update_rectangle)?; + } + _ => { + warn!("Unsupported codec ID: {}", bits.extended_bitmap_data.codec_id); + } + } + } + SurfaceCommand::FrameMarker(marker) => { + trace!( + "Frame marker: action {:?} with ID #{}", + marker.frame_action, + marker.frame_id.unwrap_or(0) + ); + self.marker_processor.process(&marker, output)?; + } + } + } + + Ok(update_rectangle.unwrap_or_else(InclusiveRectangle::empty)) + } +} + +#[cfg(feature = "qoi")] +fn qoi_apply( + image: &mut DecodedImage, + destination: InclusiveRectangle, + data: &[u8], + update_rectangle: &mut Option, +) -> SessionResult<()> { + let (header, decoded) = qoi::decode_to_vec(data).map_err(|e| reason_err!("QOI decode", "{}", e))?; + match header.channels { + qoi::Channels::Rgb => { + let rectangle = image.apply_rgb24(&decoded, &destination, false)?; + + *update_rectangle = update_rectangle + .as_ref() + .map(|rect: &InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + } + qoi::Channels::Rgba => { + warn!("Unsupported RGBA QOI data"); + } + } + Ok(()) +} + +pub struct ProcessorBuilder { + pub io_channel_id: u16, + pub user_channel_id: u16, + /// Ignore server pointer updates. + pub enable_server_pointer: bool, + /// Use software rendering mode for pointer bitmap generation. When this option is active, + /// `UpdateKind::PointerBitmap` will not be generated. Remote pointer will be drawn + /// via software rendering on top of the output image. + pub pointer_software_rendering: bool, +} + +impl ProcessorBuilder { + pub fn build(self) -> Processor { + Processor { + complete_data: CompleteData::new(), + rfx_handler: rfx::DecodingContext::new(), + marker_processor: FrameMarkerProcessor::new(self.user_channel_id, self.io_channel_id), + bitmap_stream_decoder: BitmapStreamDecoder::default(), + pointer_cache: PointerCache::default(), + use_system_pointer: true, + mouse_pos_update: None, + enable_server_pointer: self.enable_server_pointer, + pointer_software_rendering: self.pointer_software_rendering, + #[cfg(feature = "qoiz")] + zdctx: zstd_safe::DCtx::default(), + } + } +} + +#[derive(Debug, PartialEq)] +struct CompleteData { + fragmented_data: Option>, +} + +impl CompleteData { + fn new() -> Self { + Self { fragmented_data: None } + } + + fn process_data(&mut self, data: &[u8], fragmentation: Fragmentation) -> Option> { + match fragmentation { + Fragmentation::Single => { + self.check_data_is_empty(); + + Some(data.to_vec()) + } + Fragmentation::First => { + self.check_data_is_empty(); + + self.fragmented_data = Some(data.to_vec()); + + None + } + Fragmentation::Next => { + self.append_data(data); + + None + } + Fragmentation::Last => { + self.append_data(data); + + self.fragmented_data.take() + } + } + } + + fn check_data_is_empty(&mut self) { + if self.fragmented_data.is_some() { + warn!("Skipping pending Fast-Path Update internal multiple elements data"); + self.fragmented_data = None; + } + } + + fn append_data(&mut self, data: &[u8]) { + if let Some(fragmented_data) = self.fragmented_data.as_mut() { + fragmented_data.extend_from_slice(data); + } else { + warn!("Got unexpected Next fragmentation PDU without prior First fragmentation PDU"); + } + } +} + +struct FrameMarkerProcessor { + user_channel_id: u16, + io_channel_id: u16, +} + +impl FrameMarkerProcessor { + fn new(user_channel_id: u16, io_channel_id: u16) -> Self { + Self { + user_channel_id, + io_channel_id, + } + } + + fn process(&mut self, marker: &FrameMarkerPdu, output: &mut WriteBuf) -> SessionResult<()> { + match marker.frame_action { + FrameAction::Begin => Ok(()), + FrameAction::End => { + ironrdp_connector::legacy::encode_share_data( + self.user_channel_id, + self.io_channel_id, + 0, + ShareDataPdu::FrameAcknowledge(FrameAcknowledgePdu { + frame_id: marker.frame_id.unwrap_or(0), + }), + output, + ) + .map_err(crate::legacy::map_error)?; + + Ok(()) + } + } + } +} diff --git a/crates/ironrdp-session/src/image.rs b/crates/ironrdp-session/src/image.rs new file mode 100644 index 00000000..88dedb54 --- /dev/null +++ b/crates/ironrdp-session/src/image.rs @@ -0,0 +1,692 @@ +use std::sync::Arc; + +use ironrdp_core::assert_impl; +use ironrdp_graphics::color_conversion::rdp_16bit_to_rgb; +use ironrdp_graphics::image_processing::{ImageRegion, ImageRegionMut, PixelFormat}; +use ironrdp_graphics::pointer::DecodedPointer; +use ironrdp_graphics::rectangle_processing::Region; +use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; +use tracing::trace; + +use crate::{custom_err, SessionResult}; + +const TILE_SIZE: u16 = 64; + +pub struct DecodedImage { + pixel_format: PixelFormat, + data: Vec, + + /// Part of the pointer image which should be drawn + pointer_src_rect: InclusiveRectangle, + /// X position of the pointer sprite on the screen + pointer_draw_x: u16, + /// Y position of the pointer sprite on the screen + pointer_draw_y: u16, + + pointer_x: u16, + pointer_y: u16, + + pointer: Option>, + /// Image data, overridden by pointer. Used to restore image after pointer was hidden or moved + pointer_backbuffer: Vec, + /// Whether to show pointer or not + show_pointer: bool, + /// Whether pointer is visible on the screen or its sprite is currently out of bounds + pointer_visible_on_screen: bool, + + width: u16, + height: u16, +} + +assert_impl!(DecodedImage: Send); + +impl core::fmt::Debug for DecodedImage { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("DecodedImage") + .field("pixel_format", &self.pixel_format) + .field("data_len", &self.data.len()) + .field("pointer_src_rect", &self.pointer_src_rect) + .field("pointer_draw_x", &self.pointer_draw_x) + .field("pointer_draw_y", &self.pointer_draw_y) + .field("pointer_x", &self.pointer_x) + .field("pointer_y", &self.pointer_y) + .field("pointer", &self.pointer) + .field("pointer_backbuffer", &self.pointer_backbuffer) + .field("show_pointer", &self.show_pointer) + .field("pointer_visible_on_screen", &self.pointer_visible_on_screen) + .field("width", &self.width) + .field("height", &self.height) + .finish() + } +} + +#[derive(PartialEq, Eq)] +enum PointerLayer { + Background, + Pointer, +} + +struct PointerRenderingState { + redraw: bool, + update_rectangle: InclusiveRectangle, +} + +#[expect(clippy::too_many_arguments)] +fn copy_cursor_data( + from: &[u8], + from_pos: (usize, usize), + from_stride: usize, + to: &mut [u8], + to_stride: usize, + to_pos: (usize, usize), + size: (usize, usize), + dst_size: (usize, usize), + composite: bool, +) { + const PIXEL_SIZE: usize = 4; + + if to_pos.0 + size.0 > dst_size.0 || to_pos.1 + size.1 > dst_size.1 { + // Perform clipping + return; + } + + let (from_x, from_y) = from_pos; + let (to_x, to_y) = to_pos; + let (width, height) = size; + + for y in 0..height { + let from_start = (from_y + y) * from_stride + from_x * PIXEL_SIZE; + let to_start = (to_y + y) * to_stride + to_x * PIXEL_SIZE; + + if composite { + for pixel in 0..width { + let dest_r = to[to_start + pixel * PIXEL_SIZE]; + let dest_g = to[to_start + pixel * PIXEL_SIZE + 1]; + let dest_b = to[to_start + pixel * PIXEL_SIZE + 2]; + + let src_r = from[from_start + pixel * PIXEL_SIZE]; + let src_g = from[from_start + pixel * PIXEL_SIZE + 1]; + let src_b = from[from_start + pixel * PIXEL_SIZE + 2]; + let src_a = from[from_start + pixel * PIXEL_SIZE + 3]; + + // Inverted pixel, this color has a special meaning when encoded by ironrdp-graphics + if src_a == 0 && src_r == 255 && src_g == 255 && src_b == 255 { + to[to_start + pixel * PIXEL_SIZE] = 255 - dest_r; + to[to_start + pixel * PIXEL_SIZE + 1] = 255 - dest_g; + to[to_start + pixel * PIXEL_SIZE + 2] = 255 - dest_b; + to[to_start + pixel * PIXEL_SIZE + 3] = 255; + continue; + } + + // Skip 100% transparent pixels + if src_a == 0 { + continue; + } + + #[expect(clippy::as_conversions, reason = "(u16 >> 8) fits into u8 + hot loop")] + { + // Integer alpha blending, source represented as premultiplied alpha color, calculation in floating point + to[to_start + pixel * PIXEL_SIZE] = + src_r + ((u16::from(dest_r) * u16::from(255 - src_a)) >> 8) as u8; + to[to_start + pixel * PIXEL_SIZE + 1] = + src_g + ((u16::from(dest_g) * u16::from(255 - src_a)) >> 8) as u8; + to[to_start + pixel * PIXEL_SIZE + 2] = + src_b + ((u16::from(dest_b) * u16::from(255 - src_a)) >> 8) as u8; + // Framebuffer is always opaque, so we can skip alpha channel change + } + } + } else { + to[to_start..to_start + width * PIXEL_SIZE] + .copy_from_slice(&from[from_start..from_start + width * PIXEL_SIZE]); + } + } +} + +impl DecodedImage { + pub fn new(pixel_format: PixelFormat, width: u16, height: u16) -> Self { + let len = usize::from(width) * usize::from(height) * usize::from(pixel_format.bytes_per_pixel()); + + Self { + pixel_format, + data: vec![0; len], + width, + height, + + pointer_src_rect: InclusiveRectangle { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + pointer_x: 0, + pointer_y: 0, + pointer_draw_x: 0, + pointer_draw_y: 0, + pointer_backbuffer: Vec::new(), + pointer: None, + show_pointer: false, + pointer_visible_on_screen: true, + } + } + + pub fn pixel_format(&self) -> PixelFormat { + self.pixel_format + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub fn width(&self) -> u16 { + self.width + } + + pub fn bytes_per_pixel(&self) -> usize { + usize::from(self.pixel_format.bytes_per_pixel()) + } + + pub fn stride(&self) -> usize { + usize::from(self.width) * self.bytes_per_pixel() + } + + pub fn data_for_rect(&self, rect: &InclusiveRectangle) -> &[u8] { + let start = usize::from(rect.left) * self.bytes_per_pixel() + usize::from(rect.top) * self.stride(); + let end = + start + usize::from(rect.height() - 1) * self.stride() + usize::from(rect.width()) * self.bytes_per_pixel(); + &self.data[start..end] + } + + pub fn height(&self) -> u16 { + self.height + } + + fn apply_pointer_layer(&mut self, layer: PointerLayer) -> SessionResult> { + // Pointer is not hidden, but its texture is not visible on the screen, so we don't + // need to render it + if layer == PointerLayer::Pointer && !self.pointer_visible_on_screen { + return Ok(None); + } + + if self.data.is_empty() { + return Ok(None); + } + + let pointer = if let Some(pointer) = &self.pointer { + pointer + } else { + return Ok(None); + }; + + if self.pointer_src_rect.width() == 0 || self.pointer_src_rect.height() == 0 { + return Ok(None); + } + + let dest_rect = InclusiveRectangle { + left: self.pointer_draw_x, + top: self.pointer_draw_y, + right: self.pointer_draw_x + self.pointer_src_rect.width() - 1, + bottom: self.pointer_draw_y + self.pointer_src_rect.height() - 1, + }; + + if dest_rect.width() == 0 || dest_rect.height() == 0 { + return Ok(None); + } + + let pointer_src_rect_width = usize::from(self.pointer_src_rect.width()); + let pointer_src_rect_height = usize::from(self.pointer_src_rect.height()); + let pointer_draw_x = usize::from(self.pointer_draw_x); + let pointer_draw_y = usize::from(self.pointer_draw_y); + let width = usize::from(self.width); + let height = usize::from(self.height); + + match &layer { + PointerLayer::Background => { + if self.pointer_backbuffer.is_empty() { + // Backbuffer were previously empty + return Ok(None); + } + + copy_cursor_data( + &self.pointer_backbuffer, + (0, 0), + pointer_src_rect_width * 4, + &mut self.data, + width * 4, + (pointer_draw_x, pointer_draw_y), + (pointer_src_rect_width, pointer_src_rect_height), + (width, height), + false, + ); + } + PointerLayer::Pointer => { + // Copy current background to backbuffer + let buffer_size = self + .pointer_backbuffer + .len() + .max(pointer_src_rect_width * pointer_src_rect_height * 4); + self.pointer_backbuffer.resize(buffer_size, 0); + + copy_cursor_data( + &self.data, + (pointer_draw_x, pointer_draw_y), + width * 4, + &mut self.pointer_backbuffer, + pointer_src_rect_width * 4, + (0, 0), + (pointer_src_rect_width, pointer_src_rect_height), + (width, height), + false, + ); + + // Draw pointer (with compositing) + copy_cursor_data( + pointer.bitmap_data.as_slice(), + ( + usize::from(self.pointer_src_rect.left), + usize::from(self.pointer_src_rect.top), + ), + usize::from(pointer.width) * 4, + &mut self.data, + width * 4, + (pointer_draw_x, pointer_draw_y), + (pointer_src_rect_width, pointer_src_rect_height), + (width, height), + true, + ); + } + } + + // Request redraw of the changed area + Ok(Some(dest_rect)) + } + + pub(crate) fn show_pointer(&mut self) -> SessionResult> { + if !self.show_pointer { + self.show_pointer = true; + self.apply_pointer_layer(PointerLayer::Pointer) + } else { + Ok(None) + } + } + + pub(crate) fn hide_pointer(&mut self) -> SessionResult> { + if self.show_pointer { + self.show_pointer = false; + self.apply_pointer_layer(PointerLayer::Background) + } else { + Ok(None) + } + } + + fn recalculate_pointer_geometry(&mut self) { + let x = self.pointer_x; + let y = self.pointer_y; + + let pointer = match &self.pointer { + Some(pointer) if self.show_pointer => pointer, + _ => return, + }; + + let left_virtual = i32::from(x) - i32::from(pointer.hotspot_x); + let top_virtual = i32::from(y) - i32::from(pointer.hotspot_y); + let right_virtual = left_virtual + i32::from(pointer.width) - 1; + let bottom_virtual = top_virtual + i32::from(pointer.height) - 1; + + let (left, draw_x) = if left_virtual < 0 { + // Cut left side if required + (pointer.hotspot_x - x, 0) + } else { + (0, x - pointer.hotspot_x) + }; + + let (top, draw_y) = if top_virtual < 0 { + // Cut top side if required + (pointer.hotspot_y - y, 0) + } else { + (0, y - pointer.hotspot_y) + }; + + // Cut right side if required + let right = if right_virtual >= i32::from(self.width - 1) { + if draw_x + 1 >= self.width { + // Pointer is completely out of bounds horizontally + self.pointer_visible_on_screen = false; + return; + } else { + self.width - (draw_x + 1) + } + } else { + pointer.width - 1 + }; + + // Cut bottom side if required + let bottom = if bottom_virtual >= i32::from(self.height - 1) { + if (draw_y + 1) >= self.height { + // Pointer is completely out of bounds vertically + self.pointer_visible_on_screen = false; + return; + } else { + self.height - (draw_y + 1) + } + } else { + pointer.height - 1 + }; + + self.pointer_visible_on_screen = true; + + let pointer_src_rect = InclusiveRectangle { + left, + top, + right, + bottom, + }; + + self.pointer_src_rect = pointer_src_rect; + self.pointer_draw_x = draw_x; + self.pointer_draw_y = draw_y; + } + + pub(crate) fn move_pointer(&mut self, x: u16, y: u16) -> SessionResult> { + self.pointer_x = x; + self.pointer_y = y; + + if self.pointer.is_some() && self.show_pointer { + let old_rect = self.apply_pointer_layer(PointerLayer::Background)?; + self.recalculate_pointer_geometry(); + let new_rect = self.apply_pointer_layer(PointerLayer::Pointer)?; + + match (old_rect, new_rect) { + (None, None) => Ok(None), + (None, Some(rect)) => Ok(Some(rect)), + (Some(rect), None) => Ok(Some(rect)), + (Some(a), Some(b)) => Ok(Some(a.union(&b))), + } + } else { + Ok(None) + } + } + + pub(crate) fn update_pointer(&mut self, pointer: Arc) -> SessionResult> { + self.show_pointer = true; + + // Remove old pointer from frame buffer + let old_rect = if self.pointer.is_some() { + self.apply_pointer_layer(PointerLayer::Background)? + } else { + None + }; + + self.pointer = Some(pointer); + self.recalculate_pointer_geometry(); + + // Draw new pointer + let new_rect = self.apply_pointer_layer(PointerLayer::Pointer)?; + + match (old_rect, new_rect) { + (None, None) => Ok(None), + (None, Some(rect)) => Ok(Some(rect)), + (Some(rect), None) => Ok(Some(rect)), + (Some(a), Some(b)) => Ok(Some(a.union(&b))), + } + } + + fn is_pointer_redraw_required(&self, update_rectangle: &InclusiveRectangle) -> bool { + let pointer_dest_rect = InclusiveRectangle { + left: self.pointer_draw_x, + top: self.pointer_draw_y, + right: self.pointer_draw_x + self.pointer_src_rect.width() - 1, + bottom: self.pointer_draw_y + self.pointer_src_rect.height() - 1, + }; + + update_rectangle.intersect(&pointer_dest_rect).is_some() && self.show_pointer + } + + /// This method should be called BEFORE and framebuffer updates, with the update rectangle, + /// to determine if the pointer needs to be redrawn (overlapping with the update rectangle). + fn pointer_rendering_begin( + &mut self, + update_rectangle: &InclusiveRectangle, + ) -> SessionResult { + if !self.is_pointer_redraw_required(update_rectangle) || self.pointer.is_none() { + return Ok(PointerRenderingState { + redraw: false, + update_rectangle: update_rectangle.clone(), + }); + } + + let state = self + .apply_pointer_layer(PointerLayer::Background)? + .map(|cursor_erase_rect| PointerRenderingState { + redraw: true, + update_rectangle: cursor_erase_rect.union(update_rectangle), + }) + .unwrap_or_else(|| PointerRenderingState { + redraw: false, + update_rectangle: update_rectangle.clone(), + }); + + Ok(state) + } + + fn pointer_rendering_end( + &mut self, + pointer_rendering_state: PointerRenderingState, + ) -> SessionResult { + if !pointer_rendering_state.redraw { + return Ok(pointer_rendering_state.update_rectangle); + } + + let update_rectangle = self + .apply_pointer_layer(PointerLayer::Pointer)? + .map(|pointer_draw_rectangle| pointer_draw_rectangle.union(&pointer_rendering_state.update_rectangle)) + .unwrap_or_else(|| pointer_rendering_state.update_rectangle); + + Ok(update_rectangle) + } + + // To apply the buffer, we need to un-apply previously drawn cursor, and then apply it again + // in other position. + + pub(crate) fn apply_tile( + &mut self, + tile_output: &[u8], + pixel_format: PixelFormat, + clipping_rectangles: &Region, + update_rectangle: &InclusiveRectangle, + ) -> SessionResult { + trace!("Tile: {:?}", update_rectangle); + + let pointer_rendering_state = self.pointer_rendering_begin(&clipping_rectangles.extents)?; + + let update_region = clipping_rectangles.intersect_rectangle(update_rectangle); + for region_rectangle in &update_region.rectangles { + let source_x = region_rectangle.left - update_rectangle.left; + let source_y = region_rectangle.top - update_rectangle.top; + let stride = u16::from(pixel_format.bytes_per_pixel()) * TILE_SIZE; + let source_image_region = ImageRegion { + region: InclusiveRectangle { + left: source_x, + top: source_y, + right: source_x + region_rectangle.width() - 1, + bottom: source_y + region_rectangle.height() - 1, + }, + data: tile_output, + step: stride, + pixel_format, + }; + + let mut destination_image_region = ImageRegionMut { + region: region_rectangle.clone(), + step: self.width() * u16::from(self.pixel_format.bytes_per_pixel()), + pixel_format: self.pixel_format, + data: &mut self.data, + }; + + trace!("Source image region: {:?}", source_image_region.region); + trace!("Destination image region: {:?}", destination_image_region.region); + + source_image_region + .copy_to(&mut destination_image_region) + .map_err(|e| custom_err!("copy_to", e))?; + } + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + // FIXME: this assumes PixelFormat::RgbA32 + pub(crate) fn apply_rgb16_bitmap( + &mut self, + rgb16: &[u8], + update_rectangle: &InclusiveRectangle, + ) -> SessionResult { + const SRC_COLOR_DEPTH: usize = 2; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let rectangle_width = usize::from(update_rectangle.width()); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + rgb16 + .chunks_exact(rectangle_width * SRC_COLOR_DEPTH) + .rev() + .enumerate() + .for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let rgb16_value = u16::from_le_bytes( + src_pixel + .try_into() + .expect("src_pixel contains exactly two u8 elements"), + ); + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + let [r, g, b] = rdp_16bit_to_rgb(rgb16_value); + self.data[dst_idx] = r; + self.data[dst_idx + 1] = g; + self.data[dst_idx + 2] = b; + self.data[dst_idx + 3] = 0xff; + }) + }); + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + // FIXME: this assumes PixelFormat::RgbA32 + fn apply_rgb24_iter<'a, I>( + &mut self, + rgb24: I, + update_rectangle: &InclusiveRectangle, + ) -> SessionResult + where + I: Iterator, + { + const SRC_COLOR_DEPTH: usize = 3; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + rgb24.enumerate().for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + // Copy RGB channels as is + self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(src_pixel); + // Set alpha channel to opaque(0xFF) + self.data[dst_idx + 3] = 0xFF; + }) + }); + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + pub(crate) fn apply_rgb24( + &mut self, + rgb24: &[u8], + update_rectangle: &InclusiveRectangle, + flip: bool, + ) -> SessionResult { + const SRC_COLOR_DEPTH: usize = 3; + let rectangle_width = usize::from(update_rectangle.width()); + let lines = rgb24.chunks_exact(rectangle_width * SRC_COLOR_DEPTH); + if flip { + self.apply_rgb24_iter(lines.rev(), update_rectangle) + } else { + self.apply_rgb24_iter(lines, update_rectangle) + } + } + + // FIXME: this assumes PixelFormat::RgbA32 + pub(crate) fn apply_rgb32_bitmap( + &mut self, + rgb32: &[u8], + format: PixelFormat, + update_rectangle: &InclusiveRectangle, + ) -> SessionResult { + const SRC_COLOR_DEPTH: usize = 4; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let rectangle_width = usize::from(update_rectangle.width()); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + if format == self.pixel_format { + rgb32 + .chunks_exact(rectangle_width * SRC_COLOR_DEPTH) + .rev() + .enumerate() + .for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(src_pixel); + }) + }); + } else { + rgb32 + .chunks_exact(rectangle_width * SRC_COLOR_DEPTH) + .rev() + .enumerate() + .try_for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .enumerate() + .try_for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + let c = format + .read_color(src_pixel) + .map_err(|err| custom_err!("read color", err))?; + self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(&[c.r, c.g, c.b, c.a]); + + Ok(()) + })?; + + Ok(()) + })?; + } + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } +} diff --git a/crates/ironrdp-session/src/legacy.rs b/crates/ironrdp-session/src/legacy.rs new file mode 100644 index 00000000..44e1399f --- /dev/null +++ b/crates/ironrdp-session/src/legacy.rs @@ -0,0 +1,18 @@ +use crate::SessionError; + +// FIXME: code should be fixed so that we never need this conversion +// For that, some code from this ironrdp_session::legacy and ironrdp_connector::legacy modules should be moved to ironrdp_pdu itself +impl From for crate::SessionErrorKind { + fn from(value: ironrdp_connector::ConnectorErrorKind) -> Self { + match value { + ironrdp_connector::ConnectorErrorKind::Custom | ironrdp_connector::ConnectorErrorKind::Credssp(_) => { + crate::SessionErrorKind::Custom + } + _ => crate::SessionErrorKind::General, + } + } +} + +pub(crate) fn map_error(error: ironrdp_connector::ConnectorError) -> SessionError { + error.into_other_kind() +} diff --git a/crates/ironrdp-session/src/lib.rs b/crates/ironrdp-session/src/lib.rs new file mode 100644 index 00000000..8b3d62a4 --- /dev/null +++ b/crates/ironrdp-session/src/lib.rs @@ -0,0 +1,124 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![allow(clippy::arithmetic_side_effects)] // FIXME: remove + +mod macros; + +pub mod fast_path; +pub mod image; +pub mod legacy; +pub mod pointer; +pub mod rfx; // FIXME: maybe this module should not be in this crate +pub mod x224; + +mod active_stage; + +use core::fmt; + +pub use active_stage::{ActiveStage, ActiveStageOutput, GracefulDisconnectReason}; + +pub type SessionResult = Result; + +#[non_exhaustive] +#[derive(Debug)] +pub enum SessionErrorKind { + Pdu(ironrdp_pdu::PduError), + Encode(ironrdp_core::EncodeError), + Decode(ironrdp_core::DecodeError), + Reason(String), + General, + Custom, +} + +impl fmt::Display for SessionErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + SessionErrorKind::Pdu(_) => write!(f, "PDU error"), + SessionErrorKind::Encode(_) => write!(f, "encode error"), + SessionErrorKind::Decode(_) => write!(f, "decode error"), + SessionErrorKind::Reason(description) => write!(f, "reason: {description}"), + SessionErrorKind::General => write!(f, "general error"), + SessionErrorKind::Custom => write!(f, "custom error"), + } + } +} + +impl core::error::Error for SessionErrorKind { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match &self { + SessionErrorKind::Pdu(e) => Some(e), + SessionErrorKind::Encode(e) => Some(e), + SessionErrorKind::Decode(e) => Some(e), + SessionErrorKind::Reason(_) => None, + SessionErrorKind::General => None, + SessionErrorKind::Custom => None, + } + } +} + +pub type SessionError = ironrdp_error::Error; + +pub trait SessionErrorExt { + fn pdu(error: ironrdp_pdu::PduError) -> Self; + fn encode(error: ironrdp_core::EncodeError) -> Self; + fn decode(error: ironrdp_core::DecodeError) -> Self; + fn general(context: &'static str) -> Self; + fn reason(context: &'static str, reason: impl Into) -> Self; + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static; +} + +impl SessionErrorExt for SessionError { + fn pdu(error: ironrdp_pdu::PduError) -> Self { + Self::new("payload error", SessionErrorKind::Pdu(error)) + } + + fn encode(error: ironrdp_core::EncodeError) -> Self { + Self::new("encode error", SessionErrorKind::Encode(error)) + } + + fn decode(error: ironrdp_core::DecodeError) -> Self { + Self::new("decode error", SessionErrorKind::Decode(error)) + } + + fn general(context: &'static str) -> Self { + Self::new(context, SessionErrorKind::General) + } + + fn reason(context: &'static str, reason: impl Into) -> Self { + Self::new(context, SessionErrorKind::Reason(reason.into())) + } + + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static, + { + Self::new(context, SessionErrorKind::Custom).with_source(e) + } +} + +pub trait SessionResultExt { + #[must_use] + fn with_context(self, context: &'static str) -> Self; + #[must_use] + fn with_source(self, source: E) -> Self + where + E: core::error::Error + Sync + Send + 'static; +} + +impl SessionResultExt for SessionResult { + fn with_context(self, context: &'static str) -> Self { + self.map_err(|mut e| { + e.set_context(context); + e + }) + } + + fn with_source(self, source: E) -> Self + where + E: core::error::Error + Sync + Send + 'static, + { + self.map_err(|e| e.with_source(source)) + } +} diff --git a/crates/ironrdp-session/src/macros.rs b/crates/ironrdp-session/src/macros.rs new file mode 100644 index 00000000..b612ced6 --- /dev/null +++ b/crates/ironrdp-session/src/macros.rs @@ -0,0 +1,61 @@ +/// Creates a `SessionError` with `General` kind +/// +/// Shorthand for +/// ```ignore +/// ::general(context) +/// ``` +#[macro_export] +macro_rules! general_err { + ( $context:expr $(,)? ) => {{ + <$crate::SessionError as $crate::SessionErrorExt>::general($context) + }}; +} + +/// Creates a `SessionError` with `Reason` kind +/// +/// Shorthand for +/// ```ignore +/// ::reason(context, reason) +/// ``` +#[macro_export] +macro_rules! reason_err { + ( $context:expr, $($arg:tt)* ) => {{ + <$crate::SessionError as $crate::SessionErrorExt>::reason($context, format!($($arg)*)) + }}; +} + +/// Creates a `SessionError` with `Custom` kind and a source error attached to it +/// +/// Shorthand for +/// ```ignore +/// ::custom(context, source) +/// ``` +#[macro_export] +macro_rules! custom_err { + ( $context:expr, $source:expr $(,)? ) => {{ + <$crate::SessionError as $crate::SessionErrorExt>::custom($context, $source) + }}; +} + +#[macro_export] +macro_rules! eof_try { + ($e:expr) => { + match $e { + Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => { + return Ok(None); + } + result => result, + } + }; +} + +#[macro_export] +macro_rules! try_ready { + ($e:expr) => { + match $e { + Ok(Some(v)) => Ok(v), + Ok(None) => return Ok(None), + Err(e) => Err(e), + } + }; +} diff --git a/crates/ironrdp-session/src/pointer.rs b/crates/ironrdp-session/src/pointer.rs new file mode 100644 index 00000000..192ae2fa --- /dev/null +++ b/crates/ironrdp-session/src/pointer.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use ironrdp_graphics::pointer::DecodedPointer; + +#[derive(Debug, Clone, Default)] +pub struct PointerCache { + // TODO(@pacancoder) maybe use Vec> instead? + cache: HashMap>, +} + +impl PointerCache { + pub fn insert(&mut self, id: usize, pointer: Arc) -> Option> { + self.cache.insert(id, pointer) + } + + pub fn get(&self, id: usize) -> Option> { + self.cache.get(&id).cloned() + } + + pub fn is_cached(&self, id: usize) -> bool { + self.cache.contains_key(&id) + } +} diff --git a/crates/ironrdp-session/src/rfx.rs b/crates/ironrdp-session/src/rfx.rs new file mode 100644 index 00000000..4f115a6b --- /dev/null +++ b/crates/ironrdp-session/src/rfx.rs @@ -0,0 +1,286 @@ +use core::cmp::min; + +use ironrdp_graphics::color_conversion::{self, YCbCrBuffer}; +use ironrdp_graphics::image_processing::PixelFormat; +use ironrdp_graphics::rectangle_processing::Region; +use ironrdp_graphics::{dwt, quantization, rlgr, subband_reconstruction}; +use ironrdp_pdu::codecs::rfx::{self, EntropyAlgorithm, Quant, RfxRectangle, Tile}; +use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; +use ironrdp_pdu::{decode_cursor, Decode as _, ReadCursor}; +use tracing::{instrument, trace}; + +use crate::image::DecodedImage; +use crate::{custom_err, general_err, reason_err, SessionResult}; + +const TILE_SIZE: u16 = 64; + +pub type FrameId = u32; + +pub struct DecodingContext { + context: rfx::ContextPdu, + channels: rfx::ChannelsPdu, + decoding_tiles: DecodingTileContext, +} + +impl Default for DecodingContext { + fn default() -> Self { + Self { + context: rfx::ContextPdu { + flags: rfx::OperatingMode::empty(), + entropy_algorithm: EntropyAlgorithm::Rlgr1, + }, + channels: rfx::ChannelsPdu(Vec::new()), + decoding_tiles: DecodingTileContext::new(), + } + } +} + +impl DecodingContext { + pub fn new() -> Self { + Self::default() + } + + pub fn decode( + &mut self, + image: &mut DecodedImage, + destination: &InclusiveRectangle, + input: &mut ReadCursor<'_>, + ) -> SessionResult<(FrameId, InclusiveRectangle)> { + loop { + let block = rfx::Block::decode(input).map_err(|e| custom_err!("decode block", e))?; + + match block { + rfx::Block::Sync(_) => { + self.process_sync(input)?; + } + rfx::Block::CodecChannel(rfx::CodecChannel::FrameBegin(f)) => { + return self.process_frame(f, input, image, destination); + } + _ => { + return Err(reason_err!( + "rfx::DecodingContext", + "unexpected RFX block type: {:?}", + block.block_type() + )); + } + } + } + } + + fn process_sync(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> { + self.process_headers(input) + } + + fn process_headers(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> { + let mut context = None; + let mut channels = None; + + // headers can appear in any order: CodecVersions, Channels, Context + for _ in 0..3 { + match decode_cursor(input).map_err(|e| custom_err!("decode headers", e))? { + rfx::Block::CodecChannel(rfx::CodecChannel::Context(c)) => context = Some(c), + rfx::Block::Channels(c) => channels = Some(c), + rfx::Block::CodecVersions(_) => (), + _ => { + return Err(general_err!("unexpected RFX block type")); + } + } + } + + let context = context.ok_or_else(|| general_err!("context header is missing"))?; + let channels = channels.ok_or_else(|| general_err!("channels header is missing"))?; + + if channels.0.is_empty() { + return Err(general_err!("no RFX channel announced")); + } + + self.context = context; + self.channels = channels; + + Ok(()) + } + + #[instrument(skip_all)] + fn process_frame( + &mut self, + frame_begin: rfx::FrameBeginPdu, + input: &mut ReadCursor<'_>, + image: &mut DecodedImage, + destination: &InclusiveRectangle, + ) -> SessionResult<(FrameId, InclusiveRectangle)> { + let channel = self + .channels + .0 + .first() + .ok_or_else(|| general_err!("no RFX channel found"))?; + let width = channel.width.try_into().map_err(|_| general_err!("invalid width"))?; + let height = channel.height.try_into().map_err(|_| general_err!("invalid height"))?; + let entropy_algorithm = self.context.entropy_algorithm; + + let region: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode region", e))?; + let mut region = match region { + rfx::Block::CodecChannel(rfx::CodecChannel::Region(region)) => region, + _ => return Err(general_err!("unexpected block type")), + }; + let tile_set: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode tile_set", e))?; + let tile_set = match tile_set { + rfx::Block::CodecChannel(rfx::CodecChannel::TileSet(t)) => t, + _ => return Err(general_err!("unexpected block type")), + }; + let frame_end: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode frame_end", e))?; + if !matches!(frame_end, rfx::Block::CodecChannel(rfx::CodecChannel::FrameEnd(_))) { + return Err(general_err!("unexpected block type")); + } + + if region.rectangles.is_empty() { + region.rectangles = vec![RfxRectangle { + x: 0, + y: 0, + width, + height, + }]; + } + let region = region; + + trace!(frame_index = frame_begin.index); + trace!(destination_rectangle = ?destination); + trace!(context = ?self.context); + trace!(channels = ?self.channels); + trace!(?region); + + let clipping_rectangles = clipping_rectangles(region.rectangles.as_slice(), destination, width, height); + trace!("Clipping rectangles: {:?}", clipping_rectangles); + + let mut final_update_rectangle = clipping_rectangles.extents.clone(); + + for (update_rectangle, tile_data) in tiles_to_rectangles(tile_set.tiles.as_slice(), destination) + .zip(map_tiles_data(tile_set.tiles.as_slice(), tile_set.quants.as_slice())) + { + decode_tile( + &tile_data, + entropy_algorithm, + self.decoding_tiles.tile_output.as_mut(), + self.decoding_tiles.ycbcr_buffer.as_mut(), + self.decoding_tiles.ycbcr_temp_buffer.as_mut(), + )?; + + let current_update_rectangle = image.apply_tile( + &self.decoding_tiles.tile_output, + PixelFormat::RgbA32, + &clipping_rectangles, + &update_rectangle, + )?; + + final_update_rectangle = final_update_rectangle.union(¤t_update_rectangle); + } + + Ok((frame_begin.index, final_update_rectangle)) + } +} + +#[derive(Debug, Clone)] +struct DecodingTileContext { + tile_output: Vec, + ycbcr_buffer: Vec>, + ycbcr_temp_buffer: Vec, +} + +impl DecodingTileContext { + fn new() -> Self { + let tile_size = usize::from(TILE_SIZE); + Self { + tile_output: vec![0; tile_size * tile_size * 4], + ycbcr_buffer: vec![vec![0; tile_size * tile_size]; 3], + ycbcr_temp_buffer: vec![0; tile_size * tile_size], + } + } +} + +fn decode_tile( + tile: &TileData<'_>, + entropy_algorithm: EntropyAlgorithm, + output: &mut [u8], + ycbcr_temp: &mut [Vec], + temp: &mut [i16], +) -> SessionResult<()> { + for ((quant, data), ycbcr_buffer) in tile.quants.iter().zip(tile.data.iter()).zip(ycbcr_temp.iter_mut()) { + decode_component(quant, entropy_algorithm, data, ycbcr_buffer.as_mut_slice(), temp)?; + } + + let ycbcr_buffer = YCbCrBuffer { + y: ycbcr_temp[0].as_slice(), + cb: ycbcr_temp[1].as_slice(), + cr: ycbcr_temp[2].as_slice(), + }; + + color_conversion::ycbcr_to_rgba(ycbcr_buffer, output).map_err(|e| custom_err!("decode_tile", e))?; + + Ok(()) +} + +fn decode_component( + quant: &Quant, + entropy_algorithm: EntropyAlgorithm, + data: &[u8], + output: &mut [i16], + temp: &mut [i16], +) -> SessionResult<()> { + rlgr::decode(entropy_algorithm, data, output).map_err(|e| custom_err!("decode_component", e))?; + subband_reconstruction::decode(&mut output[4032..]); + quantization::decode(output, quant); + dwt::decode(output, temp); + + Ok(()) +} + +fn clipping_rectangles( + rectangles: &[RfxRectangle], + destination: &InclusiveRectangle, + width: u16, + height: u16, +) -> Region { + let mut clipping_rectangles = Region::new(); + + rectangles + .iter() + .map(|r| InclusiveRectangle { + left: min(destination.left + r.x, width - 1), + top: min(destination.top + r.y, height - 1), + right: min(destination.left + r.x + r.width - 1, width - 1), + bottom: min(destination.top + r.y + r.height - 1, height - 1), + }) + .for_each(|r| clipping_rectangles.union_rectangle(r)); + + clipping_rectangles +} + +fn tiles_to_rectangles<'a>( + tiles: &'a [Tile<'_>], + destination: &'a InclusiveRectangle, +) -> impl Iterator + 'a { + tiles.iter().map(|t| InclusiveRectangle { + left: destination.left + t.x * TILE_SIZE, + top: destination.top + t.y * TILE_SIZE, + right: destination.left + t.x * TILE_SIZE + TILE_SIZE - 1, + bottom: destination.top + t.y * TILE_SIZE + TILE_SIZE - 1, + }) +} + +fn map_tiles_data<'a>(tiles: &[Tile<'a>], quants: &[Quant]) -> Vec> { + tiles + .iter() + .map(|t| TileData { + quants: [ + quants[usize::from(t.y_quant_index)].clone(), + quants[usize::from(t.cb_quant_index)].clone(), + quants[usize::from(t.cr_quant_index)].clone(), + ], + data: [t.y_data, t.cb_data, t.cr_data], + }) + .collect() +} + +struct TileData<'a> { + quants: [Quant; 3], + data: [&'a [u8]; 3], +} diff --git a/crates/ironrdp-session/src/x224/mod.rs b/crates/ironrdp-session/src/x224/mod.rs new file mode 100644 index 00000000..d82793bb --- /dev/null +++ b/crates/ironrdp-session/src/x224/mod.rs @@ -0,0 +1,197 @@ +use ironrdp_connector::connection_activation::ConnectionActivationSequence; +use ironrdp_connector::legacy::SendDataIndicationCtx; +use ironrdp_core::WriteBuf; +use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel}; +use ironrdp_pdu::mcs::{DisconnectProviderUltimatum, DisconnectReason, McsMessage}; +use ironrdp_pdu::rdp::headers::ShareDataPdu; +use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu}; +use ironrdp_pdu::x224::X224; +use ironrdp_svc::{client_encode_svc_messages, StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages}; +use tracing::debug; + +use crate::{reason_err, SessionError, SessionErrorExt as _, SessionResult}; + +/// X224 Processor output +#[derive(Debug, Clone)] +pub enum ProcessorOutput { + /// A buffer with encoded data to send to the server. + ResponseFrame(Vec), + /// A graceful disconnect notification. Client should close the connection upon receiving this. + Disconnect(DisconnectDescription), + /// Received a [`ironrdp_pdu::rdp::headers::ServerDeactivateAll`] PDU. Client should execute the + /// [Deactivation-Reactivation Sequence]. + /// + /// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432 + DeactivateAll(Box), +} + +#[derive(Debug, Clone)] +pub enum DisconnectDescription { + /// Includes the reason from the MCS Disconnect Provider Ultimatum. + /// This is the least-specific disconnect reason and is only used + /// when a more specific disconnect code is not available. + McsDisconnect(DisconnectReason), + + /// Includes the error information sent by the RDP server when there + /// is a connection or disconnection failure. + ErrorInfo(ErrorInfo), +} + +pub struct Processor { + static_channels: StaticChannelSet, + user_channel_id: u16, + io_channel_id: u16, + connection_activation: ConnectionActivationSequence, +} + +impl Processor { + pub fn new( + static_channels: StaticChannelSet, + user_channel_id: u16, + io_channel_id: u16, + connection_activation: ConnectionActivationSequence, + ) -> Self { + Self { + static_channels, + user_channel_id, + io_channel_id, + connection_activation, + } + } + + pub fn get_svc_processor(&self) -> Option<&T> { + self.static_channels + .get_by_type::() + .and_then(|svc| svc.channel_processor_downcast_ref()) + } + + pub fn get_svc_processor_mut(&mut self) -> Option<&mut T> { + self.static_channels + .get_by_type_mut::() + .and_then(|svc| svc.channel_processor_downcast_mut()) + } + + /// Completes user's SVC request with data, required to sent it over the network and returns + /// a buffer with encoded data. + pub fn process_svc_processor_messages( + &self, + messages: SvcProcessorMessages, + ) -> SessionResult> { + let channel_id = self + .static_channels + .get_channel_id_by_type::() + .ok_or_else(|| reason_err!("SVC", "channel not found"))?; + + process_svc_messages(messages.into(), channel_id, self.user_channel_id) + } + + pub fn get_dvc(&self) -> Option<&DynamicVirtualChannel> { + self.get_svc_processor::()?.get_dvc_by_type_id::() + } + + pub fn get_dvc_by_channel_id(&self, channel_id: u32) -> Option<&DynamicVirtualChannel> { + self.get_svc_processor::()? + .get_dvc_by_channel_id(channel_id) + } + + /// Processes a received PDU. Returns a vector of [`ProcessorOutput`] that must be processed + /// in the returned order. + pub fn process(&mut self, frame: &[u8]) -> SessionResult> { + let data_ctx: SendDataIndicationCtx<'_> = + ironrdp_connector::legacy::decode_send_data_indication(frame).map_err(crate::legacy::map_error)?; + let channel_id = data_ctx.channel_id; + + if channel_id == self.io_channel_id { + self.process_io_channel(data_ctx) + } else if let Some(svc) = self.static_channels.get_by_channel_id_mut(channel_id) { + let response_pdus = svc.process(data_ctx.user_data).map_err(SessionError::pdu)?; + process_svc_messages(response_pdus, channel_id, data_ctx.initiator_id) + .map(|data| vec![ProcessorOutput::ResponseFrame(data)]) + } else { + Err(reason_err!("X224", "unexpected channel received: ID {channel_id}")) + } + } + + fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult> { + debug_assert_eq!(data_ctx.channel_id, self.io_channel_id); + + let io_channel = ironrdp_connector::legacy::decode_io_channel(data_ctx).map_err(crate::legacy::map_error)?; + + match io_channel { + ironrdp_connector::legacy::IoChannelPdu::Data(ctx) => { + match ctx.pdu { + ShareDataPdu::SaveSessionInfo(session_info) => { + debug!("Got Session Save Info PDU: {session_info:?}"); + Ok(Vec::new()) + } + // FIXME: workaround fix to not terminate the session on "unhandled PDU: Set Keyboard Indicators PDU" + ShareDataPdu::SetKeyboardIndicators(data) => { + debug!("Got Keyboard Indicators PDU: {data:?}"); + Ok(Vec::new()) + } + ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode( + ProtocolIndependentCode::None, + ))) => { + debug!("Received None server error"); + Ok(Vec::new()) + } + ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(e)) => { + // This is a part of server-side graceful disconnect procedure defined + // in [MS-RDPBCGR]. + // + // [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/149070b0-ecec-4c20-af03-934bbc48adb8 + let desc = DisconnectDescription::ErrorInfo(e); + Ok(vec![ProcessorOutput::Disconnect(desc)]) + } + ShareDataPdu::ShutdownDenied => { + debug!("ShutdownDenied received, session will be closed"); + + // As defined in [MS-RDPBCGR], when `ShareDataPdu::ShutdownDenied` is received, we + // need to send a disconnect ultimatum to the server if we want to proceed with the + // session shutdown. + // + // [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68 + let ultimatum = McsMessage::DisconnectProviderUltimatum( + DisconnectProviderUltimatum::from_reason(DisconnectReason::UserRequested), + ); + + let encoded_pdu = ironrdp_core::encode_vec(&X224(ultimatum)).map_err(SessionError::encode); + + Ok(vec![ + ProcessorOutput::ResponseFrame(encoded_pdu?), + ProcessorOutput::Disconnect(DisconnectDescription::McsDisconnect( + DisconnectReason::UserRequested, + )), + ]) + } + _ => Err(reason_err!( + "IO channel", + "unhandled PDU: {:?}", + ctx.pdu.as_short_name() + )), + } + } + ironrdp_connector::legacy::IoChannelPdu::DeactivateAll(_) => Ok(vec![ProcessorOutput::DeactivateAll( + Box::new(self.connection_activation.reset_clone()), + )]), + } + } + + /// Send a pdu on the static global channel. Typically used to send input events + pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult { + let written = + ironrdp_connector::legacy::encode_share_data(self.user_channel_id, self.io_channel_id, 0, pdu, output) + .map_err(crate::legacy::map_error)?; + Ok(written) + } +} + +/// Processes a vector of [`SvcMessage`] in preparation for sending them to the server on the `channel_id` channel. +/// +/// This includes chunkifying the messages, adding MCS, x224, and tpkt headers, and encoding them into a buffer. +/// The messages returned here are ready to be sent to the server. +/// +/// The caller is responsible for ensuring that the `channel_id` corresponds to the correct channel. +fn process_svc_messages(messages: Vec, channel_id: u16, initiator_id: u16) -> SessionResult> { + client_encode_svc_messages(messages, channel_id, initiator_id).map_err(SessionError::encode) +} diff --git a/crates/ironrdp-svc/CHANGELOG.md b/crates/ironrdp-svc/CHANGELOG.md new file mode 100644 index 00000000..7e650f78 --- /dev/null +++ b/crates/ironrdp-svc/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-svc-v0.4.0...ironrdp-svc-v0.4.1)] - 2025-06-27 + +### Features + +- Implement `fmt::Debug` of `SvcMessage` (#791) ([5482365655](https://github.com/Devolutions/IronRDP/commit/5482365655e5c171cd967eda401b01161a9f6602)) + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-svc-v0.3.0...ironrdp-svc-v0.4.0)] - 2025-05-27 + +### Build + +- Bump bitflags from 2.9.0 to 2.9.1 in the patch group across 1 directory (#792) ([87ed315bc2](https://github.com/Devolutions/IronRDP/commit/87ed315bc28fdd2dcfea89b052fa620a7e346e5a)) + + + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-svc-v0.2.0...ironrdp-svc-v0.3.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + + + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-svc-v0.1.3...ironrdp-svc-v0.2.0)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-svc-v0.1.2...ironrdp-svc-v0.1.3)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-svc-v0.1.1...ironrdp-svc-v0.1.2)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-svc/Cargo.toml b/crates/ironrdp-svc/Cargo.toml new file mode 100644 index 00000000..048297c4 --- /dev/null +++ b/crates/ironrdp-svc/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ironrdp-svc" +version = "0.5.0" +readme = "README.md" +description = "IronRDP traits to implement RDP static virtual channels" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +std = [] + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6", features = ["alloc", "std"] } # public +bitflags = "2.9" + +[lints] +workspace = true + diff --git a/crates/ironrdp-svc/LICENSE-APACHE b/crates/ironrdp-svc/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-svc/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-svc/LICENSE-MIT b/crates/ironrdp-svc/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-svc/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-svc/README.md b/crates/ironrdp-svc/README.md new file mode 100644 index 00000000..dd8813d3 --- /dev/null +++ b/crates/ironrdp-svc/README.md @@ -0,0 +1,7 @@ +# IronRDP SVC + +IronRDP traits to implement RDP static virtual channels. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-svc/src/lib.rs b/crates/ironrdp-svc/src/lib.rs new file mode 100644 index 00000000..e1ef1b5a --- /dev/null +++ b/crates/ironrdp-svc/src/lib.rs @@ -0,0 +1,678 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +// TODO: #![warn(missing_docs)] + +extern crate alloc; + +use alloc::collections::BTreeMap; +use core::any::TypeId; +use core::fmt; +use core::marker::PhantomData; +use std::borrow::Cow; + +use bitflags::bitflags; +use ironrdp_core::{ + assert_obj_safe, decode_cursor, encode_buf, AsAny, DecodeResult, Encode, EncodeResult, ReadCursor, WriteBuf, + WriteCursor, +}; +use ironrdp_pdu::gcc::{ChannelDef, ChannelName, ChannelOptions}; +use ironrdp_pdu::rdp::vc::ChannelControlFlags; +use ironrdp_pdu::x224::X224; +use ironrdp_pdu::{decode_err, mcs, PduResult}; + +// Re-export ironrdp_pdu crate for convenience +#[rustfmt::skip] // Do not re-order this pub use. +pub use ironrdp_pdu as pdu; + +// TODO(#583): Remove once Teleport migrated to the newer item paths. +#[doc(hidden)] +#[deprecated(since = "0.1.0", note = "use ironrdp-core")] +#[rustfmt::skip] // Do not re-order this pub use. +pub use ironrdp_core::impl_as_any; + +// NOTE: We may re-consider moving some types dedicated to SVC out of ironrdp_pdu in some future major version bump. +// The idea is to reduce the amount of code required when building a static/dynamic channel to a minimum. + +/// The integer type representing a static virtual channel ID. +pub type StaticChannelId = u16; + +/// SVC data to be sent to the server. See [`SvcMessage`] for more information. +/// Usually returned by the channel-specific methods. +pub struct SvcProcessorMessages { + messages: Vec, + _channel: PhantomData

, +} + +impl SvcProcessorMessages

{ + pub fn new(messages: Vec) -> Self { + Self { + messages, + _channel: PhantomData, + } + } +} + +impl From> for SvcProcessorMessages

{ + fn from(messages: Vec) -> Self { + Self::new(messages) + } +} + +impl From> for Vec { + fn from(request: SvcProcessorMessages

) -> Self { + request.messages + } +} + +/// Represents a message that, when encoded, forms a complete PDU for a given static virtual channel, sans any Channel PDU Header. +/// In other words, this marker should be applied to a message that is ready to be chunkified (have channel PDU headers added, +/// splitting it into chunks if necessary) and wrapped in MCS, x224, and tpkt headers for sending over the wire. +pub trait SvcEncode: Encode + Send {} + +/// For legacy reasons, we implement [`SvcEncode`] for [`Vec`]. +// FIXME: legacy code +impl SvcEncode for Vec {} + +/// Encodable PDU to be sent over a static virtual channel. +/// +/// Additional SVC header flags can be added via [`SvcMessage::with_flags`] method. +pub struct SvcMessage { + pdu: Box, + flags: ChannelFlags, +} + +impl fmt::Debug for SvcMessage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SvcMessage") + .field("pdu", &self.pdu.name()) + .field("flags", &self.flags) + .finish() + } +} + +impl SvcMessage { + /// Adds additional SVC header flags to the message. + #[must_use] + pub fn with_flags(mut self, flags: ChannelFlags) -> Self { + self.flags |= flags; + self + } +} + +impl From for SvcMessage +where + T: SvcEncode + 'static, +{ + fn from(pdu: T) -> Self { + Self { + pdu: Box::new(pdu), + flags: ChannelFlags::empty(), + } + } +} + +/// Defines which compression flag should be sent along the [`ChannelDef`] structure (CHANNEL_DEF) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompressionCondition { + /// Virtual channel data will not be compressed + Never, + /// Virtual channel data MUST be compressed if RDP data is being compressed (CHANNEL_OPTION_COMPRESS_RDP) + WhenRdpDataIsCompressed, + /// Virtual channel data MUST be compressed, regardless of RDP compression settings (CHANNEL_OPTION_COMPRESS) + Always, +} + +/// A static virtual channel. +#[derive(Debug)] +pub struct StaticVirtualChannel { + channel_processor: Box, + chunk_processor: ChunkProcessor, +} + +impl StaticVirtualChannel { + pub fn new(channel_processor: T) -> Self { + Self { + channel_processor: Box::new(channel_processor), + chunk_processor: ChunkProcessor::new(), + } + } + + pub fn channel_name(&self) -> ChannelName { + self.channel_processor.channel_name() + } + + pub fn compression_condition(&self) -> CompressionCondition { + self.channel_processor.compression_condition() + } + + pub fn start(&mut self) -> PduResult> { + self.channel_processor.start() + } + + /// Processes a payload received on the virtual channel. Returns a vector of PDUs to be sent back + /// to the server. If no PDUs are to be sent, an empty vector is returned. + pub fn process(&mut self, payload: &[u8]) -> PduResult> { + if let Some(payload) = self.dechunkify(payload).map_err(|e| decode_err!(e))? { + return self.channel_processor.process(&payload); + } + + Ok(Vec::new()) + } + + pub fn chunkify(messages: Vec) -> EncodeResult> { + ChunkProcessor::chunkify(messages, CHANNEL_CHUNK_LENGTH) + } + + pub fn channel_processor_downcast_ref(&self) -> Option<&T> { + self.channel_processor.as_any().downcast_ref() + } + + pub fn channel_processor_downcast_mut(&mut self) -> Option<&mut T> { + self.channel_processor.as_any_mut().downcast_mut() + } + + fn dechunkify(&mut self, payload: &[u8]) -> DecodeResult>> { + self.chunk_processor.dechunkify(payload) + } +} + +fn encode_svc_messages( + messages: Vec, + channel_id: u16, + initiator_id: u16, + client: bool, +) -> EncodeResult> { + let mut fully_encoded_responses = WriteBuf::new(); // TODO(perf): reuse this buffer using `clear` and `filled` as appropriate + + // For each response PDU, chunkify it and add appropriate static channel headers. + let chunks = StaticVirtualChannel::chunkify(messages)?; + + // SendData is [`McsPdu`], which is [`x224Pdu`], which is [`Encode`]. [`Encode`] for [`x224Pdu`] + // also takes care of adding the Tpkt header, so therefore we can just call `encode_buf` on each of these and + // we will create a buffer of fully encoded PDUs ready to send to the server. + // + // For example, if we had 2 chunks, our fully_encoded_responses buffer would look like: + // + // [ | tpkt | x224 | mcs::SendDataRequest | chunk 1 | tpkt | x224 | mcs::SendDataRequest | chunk 2 | ] + // |<------------------- PDU 1 ------------------>|<------------------- PDU 2 ------------------>| + if client { + for chunk in chunks { + let pdu = mcs::SendDataRequest { + initiator_id, + channel_id, + user_data: Cow::Borrowed(chunk.filled()), + }; + encode_buf(&X224(pdu), &mut fully_encoded_responses)?; + } + } else { + for chunk in chunks { + let pdu = mcs::SendDataIndication { + initiator_id, + channel_id, + user_data: Cow::Borrowed(chunk.filled()), + }; + encode_buf(&X224(pdu), &mut fully_encoded_responses)?; + } + } + + Ok(fully_encoded_responses.into_inner()) +} + +/// Encode a vector of [`SvcMessage`] in preparation for sending them on the `channel_id` channel. +/// +/// This includes chunkifying the messages, adding MCS, x224, and tpkt headers, and encoding them into a buffer. +/// The messages returned here are ready to be sent to the server. +/// +/// The caller is responsible for ensuring that the `channel_id` corresponds to the correct channel. +pub fn client_encode_svc_messages( + messages: Vec, + channel_id: u16, + initiator_id: u16, +) -> EncodeResult> { + encode_svc_messages(messages, channel_id, initiator_id, true) +} + +/// Encode a vector of [`SvcMessage`] in preparation for sending them on the `channel_id` channel. +/// +/// This includes chunkifying the messages, adding MCS, x224, and tpkt headers, and encoding them into a buffer. +/// The messages returned here are ready to be sent to the client. +/// +/// The caller is responsible for ensuring that the `channel_id` corresponds to the correct channel. +pub fn server_encode_svc_messages( + messages: Vec, + channel_id: u16, + initiator_id: u16, +) -> EncodeResult> { + encode_svc_messages(messages, channel_id, initiator_id, false) +} + +/// A type that is a Static Virtual Channel +/// +/// Static virtual channels are created once at the beginning of the RDP session and allow lossless +/// communication between client and server components over the main data connection. +/// There are at most 31 (optional) static virtual channels that can be created for a single connection, for a +/// total of 32 static channels when accounting for the non-optional I/O channel. +pub trait SvcProcessor: AsAny + fmt::Debug + Send { + /// Returns the name of the static virtual channel corresponding to this processor. + fn channel_name(&self) -> ChannelName; + + /// Defines which compression flag should be sent along the [`ChannelDef`] Definition Structure (`CHANNEL_DEF`) + fn compression_condition(&self) -> CompressionCondition { + CompressionCondition::Never + } + + /// Start a channel, after the connection is established and the channel is joined. + /// + /// Returns a list of PDUs to be sent back. + fn start(&mut self) -> PduResult> { + Ok(Vec::new()) + } + + /// Processes a payload received on the virtual channel. The `payload` is expected + /// to be a fully de-chunkified PDU. + /// + /// Returns a list of PDUs to be sent back. + fn process(&mut self, payload: &[u8]) -> PduResult>; +} + +assert_obj_safe!(SvcProcessor); + +pub trait SvcClientProcessor: SvcProcessor {} + +assert_obj_safe!(SvcClientProcessor); + +pub trait SvcServerProcessor: SvcProcessor {} + +assert_obj_safe!(SvcServerProcessor); + +/// ChunkProcessor is used to chunkify/de-chunkify static virtual channel PDUs. +#[derive(Debug)] +struct ChunkProcessor { + /// Buffer for de-chunkification of clipboard PDUs. Everything bigger than ~1600 bytes is + /// usually chunked when transferred over svc. + chunked_pdu: Vec, +} + +impl ChunkProcessor { + fn new() -> Self { + Self { + chunked_pdu: Vec::new(), + } + } + + /// Takes a vector of PDUs and breaks them into chunks prefixed with a Channel PDU Header (`CHANNEL_PDU_HEADER`). + /// + /// Each chunk is at most `max_chunk_len` bytes long (not including the Channel PDU Header). + fn chunkify(messages: Vec, max_chunk_len: usize) -> EncodeResult> { + let mut results = Vec::new(); + for message in messages { + results.extend(Self::chunkify_one(message, max_chunk_len)?); + } + Ok(results) + } + + /// Dechunkify a payload received on the virtual channel. + /// + /// If the payload is not chunked, returns the payload as-is. + /// For chunked payloads, returns `Ok(None)` until the last chunk is received, at which point + /// it returns `Ok(Some(payload))`. + fn dechunkify(&mut self, payload: &[u8]) -> DecodeResult>> { + let mut cursor = ReadCursor::new(payload); + let last = Self::process_header(&mut cursor)?; + + // Extend the chunked_pdu buffer with the payload + self.chunked_pdu.extend_from_slice(cursor.remaining()); + + // If this was an unchunked message, or the last in a series of chunks, return the payload + if last { + // Take the chunked_pdu buffer and replace it with an empty one + return Ok(Some(core::mem::take(&mut self.chunked_pdu))); + } + + // This was an intermediate chunk, return None + Ok(None) + } + + /// Returns whether this was the last chunk based on the flags in the channel header. + fn process_header(payload: &mut ReadCursor<'_>) -> DecodeResult { + let channel_header: ironrdp_pdu::rdp::vc::ChannelPduHeader = decode_cursor(payload)?; + + Ok(channel_header.flags.contains(ChannelControlFlags::FLAG_LAST)) + } + + /// Takes a single PDU and breaks it into chunks prefixed with a [`ChannelPduHeader`]. + /// + /// Each chunk is at most `max_chunk_len` bytes long (not including the Channel PDU Header). + /// + /// For example, if the PDU is 4000 bytes long and `max_chunk_len` is 1600, this function will + /// return 3 chunks, each 1600 bytes long, and the last chunk will be 800 bytes long. + /// + /// [[ Channel PDU Header | 1600 bytes of PDU data ] [ Channel PDU Header | 1600 bytes of PDU data ] [ Channel PDU Header | 800 bytes of PDU data ]] + fn chunkify_one(message: SvcMessage, max_chunk_len: usize) -> EncodeResult> { + let mut encoded_pdu = WriteBuf::new(); // TODO(perf): reuse this buffer using `clear` and `filled` as appropriate + encode_buf(message.pdu.as_ref(), &mut encoded_pdu)?; + + let mut chunks = Vec::new(); + + let total_len = encoded_pdu.filled_len(); + let mut chunk_start_index: usize = 0; + let mut chunk_end_index = core::cmp::min(total_len, max_chunk_len); + loop { + // Create a buffer to hold this next chunk. + // TODO(perf): Reuse this buffer using `clear` and `filled` as appropriate. + // This one will be a bit trickier because we'll need to grow + // the number of chunk buffers if we run out. + let mut chunk = WriteBuf::new(); + + // Set the first and last flags if this is the first and/or last chunk for this PDU. + let first = chunk_start_index == 0; + let last = chunk_end_index == total_len; + + // Create the header for this chunk. + let header = { + let mut flags = ChannelFlags::empty(); + if first { + flags |= ChannelFlags::FIRST; + } + if last { + flags |= ChannelFlags::LAST; + } + + flags |= message.flags; + + ChannelPduHeader { + length: ironrdp_core::cast_int!(ChannelPduHeader::NAME, "length", total_len)?, + flags, + } + }; + + // Encode the header for this chunk. + encode_buf(&header, &mut chunk)?; + // Append the piece of the encoded_pdu that belongs in this chunk. + chunk.write_slice(&encoded_pdu[chunk_start_index..chunk_end_index]); + // Push the chunk onto the results. + chunks.push(chunk); + + // If this was the last chunk, we're done, return the results. + if last { + break; + } + + // Otherwise, update the chunk start and end indices for the next iteration. + chunk_start_index = chunk_end_index; + chunk_end_index = core::cmp::min(total_len, chunk_end_index.saturating_add(max_chunk_len)); + } + + Ok(chunks) + } +} + +impl Default for ChunkProcessor { + fn default() -> Self { + Self::new() + } +} + +/// Builds the [`ChannelOptions`] bitfield to be used in the [`ChannelDef`] structure. +pub fn make_channel_options(channel: &StaticVirtualChannel) -> ChannelOptions { + match channel.compression_condition() { + CompressionCondition::Never => ChannelOptions::empty(), + CompressionCondition::WhenRdpDataIsCompressed => ChannelOptions::COMPRESS_RDP, + CompressionCondition::Always => ChannelOptions::COMPRESS, + } +} + +/// Builds the [`ChannelDef`] structure containing information for this channel. +pub fn make_channel_definition(channel: &StaticVirtualChannel) -> ChannelDef { + let name = channel.channel_name(); + let options = make_channel_options(channel); + ChannelDef { name, options } +} + +/// A set holding at most one [`StaticVirtualChannel`] for any given type +/// implementing [`SvcProcessor`]. +/// +/// To ensure uniqueness, each trait object is associated to the [`TypeId`] of it’s original type. +/// Once joined, channels may have their ID attached using [`Self::attach_channel_id()`], effectively +/// associating them together. +/// +/// At this point, it’s possible to retrieve the trait object using either +/// the type ID ([`Self::get_by_type_id()`]), the original type ([`Self::get_by_type()`]) or +/// the channel ID ([`Self::get_by_channel_id()`]). +/// +/// It’s possible to downcast the trait object and to retrieve the concrete value +/// since all [`SvcProcessor`]s are also implementing the [`AsAny`] trait. +#[derive(Debug)] +pub struct StaticChannelSet { + channels: BTreeMap, + to_channel_id: BTreeMap, + to_type_id: BTreeMap, +} + +impl StaticChannelSet { + #[inline] + pub fn new() -> Self { + Self { + channels: BTreeMap::new(), + to_channel_id: BTreeMap::new(), + to_type_id: BTreeMap::new(), + } + } + + /// Inserts a [`StaticVirtualChannel`] into this [`StaticChannelSet`]. + /// + /// If a static virtual channel of this type already exists, it is returned. + pub fn insert(&mut self, val: T) -> Option { + self.channels.insert(TypeId::of::(), StaticVirtualChannel::new(val)) + } + + /// Gets a reference to a [`StaticVirtualChannel`] by looking up its internal [`SvcProcessor`]'s [`TypeId`]. + pub fn get_by_type_id(&self, type_id: TypeId) -> Option<&StaticVirtualChannel> { + self.channels.get(&type_id) + } + + /// Gets a mutable reference to a [`StaticVirtualChannel`] by looking up its internal [`SvcProcessor`]'s [`TypeId`]. + pub fn get_by_type_id_mut(&mut self, type_id: TypeId) -> Option<&mut StaticVirtualChannel> { + self.channels.get_mut(&type_id) + } + + /// Gets a reference to a [`StaticVirtualChannel`] by looking up its internal [`SvcProcessor`]'s [`TypeId`]. + pub fn get_by_type(&self) -> Option<&StaticVirtualChannel> { + self.get_by_type_id(TypeId::of::()) + } + + /// Gets a mutable reference to a [`StaticVirtualChannel`] by looking up its internal [`SvcProcessor`]'s [`TypeId`]. + pub fn get_by_type_mut(&mut self) -> Option<&mut StaticVirtualChannel> { + self.get_by_type_id_mut(TypeId::of::()) + } + + /// Gets a reference to a [`StaticVirtualChannel`] by looking up its channel name. + pub fn get_by_channel_name(&self, name: &ChannelName) -> Option<(TypeId, &StaticVirtualChannel)> { + self.iter().find(|(_, x)| x.channel_processor.channel_name() == *name) + } + + /// Gets a reference to a [`StaticVirtualChannel`] by looking up its channel ID. + pub fn get_by_channel_id(&self, channel_id: StaticChannelId) -> Option<&StaticVirtualChannel> { + self.get_type_id_by_channel_id(channel_id) + .and_then(|type_id| self.get_by_type_id(type_id)) + } + + /// Gets a mutable reference to a [`StaticVirtualChannel`] by looking up its channel ID. + pub fn get_by_channel_id_mut(&mut self, channel_id: StaticChannelId) -> Option<&mut StaticVirtualChannel> { + self.get_type_id_by_channel_id(channel_id) + .and_then(|type_id| self.get_by_type_id_mut(type_id)) + } + + /// Removes a [`StaticVirtualChannel`] from this [`StaticChannelSet`]. + /// + /// If a static virtual channel of this type existed, it will be returned. + pub fn remove_by_type_id(&mut self, type_id: TypeId) -> Option { + let svc = self.channels.remove(&type_id); + if let Some(channel_id) = self.to_channel_id.remove(&type_id) { + self.to_type_id.remove(&channel_id); + } + svc + } + + /// Removes a [`StaticVirtualChannel`] from this [`StaticChannelSet`]. + /// + /// If a static virtual channel of this type existed, it will be returned. + pub fn remove_by_type(&mut self) -> Option { + let type_id = TypeId::of::(); + self.remove_by_type_id(type_id) + } + + /// Attaches a channel ID to a static virtual channel. + /// + /// If a channel ID was already attached, it will be returned. + pub fn attach_channel_id(&mut self, type_id: TypeId, channel_id: StaticChannelId) -> Option { + self.to_type_id.insert(channel_id, type_id); + self.to_channel_id.insert(type_id, channel_id) + } + + /// Gets the attached channel ID for a given static virtual channel. + pub fn get_channel_id_by_type_id(&self, type_id: TypeId) -> Option { + self.to_channel_id.get(&type_id).copied() + } + + /// Gets the attached channel ID for a given static virtual channel. + pub fn get_channel_id_by_type(&self) -> Option { + self.get_channel_id_by_type_id(TypeId::of::()) + } + + /// Gets the [`TypeId`] of the static virtual channel associated to this channel ID. + pub fn get_type_id_by_channel_id(&self, channel_id: StaticChannelId) -> Option { + self.to_type_id.get(&channel_id).copied() + } + + /// Detaches the channel ID associated to a given static virtual channel. + pub fn detach_channel_id(&mut self, type_id: TypeId) -> Option { + if let Some(channel_id) = self.to_channel_id.remove(&type_id) { + self.to_type_id.remove(&channel_id); + Some(channel_id) + } else { + None + } + } + + #[inline] + pub fn iter(&self) -> impl Iterator { + self.channels.iter().map(|(type_id, svc)| (*type_id, svc)) + } + + #[inline] + pub fn iter_mut(&mut self) -> impl Iterator)> { + let to_channel_id = self.to_channel_id.clone(); + self.channels + .iter_mut() + .map(move |(type_id, svc)| (*type_id, svc, to_channel_id.get(type_id).copied())) + } + + #[inline] + pub fn values(&self) -> impl Iterator { + self.channels.values() + } + + #[inline] + pub fn type_ids(&self) -> impl Iterator + '_ { + self.channels.keys().copied() + } + + #[inline] + pub fn channel_ids(&self) -> impl Iterator + '_ { + self.to_channel_id.values().copied() + } + + #[inline] + pub fn clear(&mut self) { + self.channels.clear(); + self.to_channel_id.clear(); + self.to_type_id.clear(); + } +} + +impl Default for StaticChannelSet { + fn default() -> Self { + Self::new() + } +} + +/// The default maximum chunk size for virtual channel data. +/// +/// If an RDP server supports larger chunks, it will advertise +/// the larger chunk size in the `VCChunkSize` field of the +/// virtual channel capability set. +/// +/// See also: +/// - +/// - +pub const CHANNEL_CHUNK_LENGTH: usize = 1600; + +bitflags! { + /// Channel control flags, as specified in [section 2.2.6.1.1 of MS-RDPBCGR]. + /// + /// [section 2.2.6.1.1 of MS-RDPBCGR]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/f125c65e-6901-43c3-8071-d7d5aaee7ae4 + #[derive(Debug, PartialEq, Copy, Clone)] + pub struct ChannelFlags: u32 { + /// CHANNEL_FLAG_FIRST + const FIRST = 0x0000_0001; + /// CHANNEL_FLAG_LAST + const LAST = 0x0000_0002; + /// CHANNEL_FLAG_SHOW_PROTOCOL + const SHOW_PROTOCOL = 0x0000_0010; + /// CHANNEL_FLAG_SUSPEND + const SUSPEND = 0x0000_0020; + /// CHANNEL_FLAG_RESUME + const RESUME = 0x0000_0040; + /// CHANNEL_FLAG_SHADOW_PERSISTENT + const SHADOW_PERSISTENT = 0x0000_0080; + /// CHANNEL_PACKET_COMPRESSED + const COMPRESSED = 0x0020_0000; + /// CHANNEL_PACKET_AT_FRONT + const AT_FRONT = 0x0040_0000; + /// CHANNEL_PACKET_FLUSHED + const FLUSHED = 0x0080_0000; + } +} + +/// Channel PDU Header (CHANNEL_PDU_HEADER) +/// +/// Channel PDU header precedes all static virtual channel traffic +/// transmitted between an RDP client and server. +/// +/// It is specified in [section 2.2.6.1.1 of MS-RDPBCGR]. +/// +/// [section 2.2.6.1.1 of MS-RDPBCGR]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/f125c65e-6901-43c3-8071-d7d5aaee7ae4 +#[derive(Debug)] +struct ChannelPduHeader { + /// The total length of the uncompressed PDU data, + /// excluding the length of this header. + /// Note: the data can span multiple PDUs, in which + /// case each PDU in the series contains the same + /// length field. + length: u32, + flags: ChannelFlags, +} + +impl ChannelPduHeader { + const NAME: &'static str = "CHANNEL_PDU_HEADER"; + + const FIXED_PART_SIZE: usize = 4 /* len */ + 4 /* flags */; +} + +impl Encode for ChannelPduHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + dst.write_u32(self.length); + dst.write_u32(self.flags.bits()); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} diff --git a/crates/ironrdp-testsuite-core/Cargo.toml b/crates/ironrdp-testsuite-core/Cargo.toml new file mode 100644 index 00000000..e2e29a1d --- /dev/null +++ b/crates/ironrdp-testsuite-core/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "ironrdp-testsuite-core" +version = "0.0.0" +edition = "2021" +description = "IronRDP test suite" +publish = false +autotests = false + +[lib] +doctest = false +test = false + +[features] +# Internal (PRIVATE!) features used to aid testing. +# Don't rely on these whatsoever. They may disappear at any time. +# Added here because it includes/link to some files from other crates +__bench = ["dep:visibility"] + +[[test]] +name = "integration_tests_core" +path = "tests/main.rs" +harness = true + +[dependencies] +array-concat = "0.5" +expect-test = "1" +ironrdp-core.path = "../ironrdp-core" +ironrdp-pdu.path = "../ironrdp-pdu" +paste = "1" +visibility = { version = "0.1", optional = true } + +[dev-dependencies] +anyhow = "1" +expect-test.workspace = true +hex = "0.4" +ironrdp-cliprdr-format.path = "../ironrdp-cliprdr-format" +ironrdp-cliprdr.path = "../ironrdp-cliprdr" +ironrdp-connector.path = "../ironrdp-connector" +ironrdp-displaycontrol.path = "../ironrdp-displaycontrol" +ironrdp-dvc.path = "../ironrdp-dvc" +ironrdp-fuzzing.path = "../ironrdp-fuzzing" +ironrdp-graphics.path = "../ironrdp-graphics" +ironrdp-input.path = "../ironrdp-input" +ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" +ironrdp-rdpsnd.path = "../ironrdp-rdpsnd" +ironrdp-session = { path = "../ironrdp-session", features = ["qoi"] } +ironrdp-propertyset.path = "../ironrdp-propertyset" +ironrdp-rdpfile.path = "../ironrdp-rdpfile" +png = "0.18" +pretty_assertions = "1.4" +proptest.workspace = true +rstest.workspace = true + +[lints] +workspace = true diff --git a/crates/ironrdp-testsuite-core/src/capsets.rs b/crates/ironrdp-testsuite-core/src/capsets.rs new file mode 100644 index 00000000..1f9974d6 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/capsets.rs @@ -0,0 +1,348 @@ +use std::sync::LazyLock; + +use ironrdp_core::decode; +use ironrdp_pdu::rdp::capability_sets::{ + CapabilitySet, ClientConfirmActive, DemandActive, ServerDemandActive, SERVER_CHANNEL_ID, +}; + +pub const SERVER_DEMAND_ACTIVE_BUFFER: [u8; 357] = [ + 0x04, 0x00, // source descriptor length + 0x59, 0x01, // combined length + 0x52, 0x44, 0x50, 0x00, // source descriptor + 0x0d, 0x00, // capabilities count + 0x00, 0x00, // padding + 0x09, 0x00, 0x08, 0x00, 0xea, 0x03, 0xdc, 0xe2, // Share capability set, + 0x01, 0x00, 0x18, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x01, // General capability set + 0x14, 0x00, 0x0c, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00, // VirtualChannel capability set + 0x16, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xf6, 0x13, 0xf3, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x9c, 0xf6, 0x13, 0xf3, 0x61, 0xa6, 0x82, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, + 0x91, 0xbf, // DrawGdiPlus capability set + 0x0e, 0x00, 0x08, 0x00, 0x00, 0x01, 0x00, 0x00, // Font capability set + 0x02, 0x00, 0x1c, 0x00, 0x18, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // Bitmap capability set + 0x03, 0x00, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x22, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x42, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Order capability set + 0x0a, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, // ColorCache capability set + 0x12, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, // BitmapCache capability set + 0x08, 0x00, 0x0a, 0x00, 0x01, 0x00, 0x19, 0x00, 0x19, 0x00, // Pointer capability set + 0x0d, 0x00, 0x58, 0x00, 0x35, 0x00, 0x00, 0x00, 0xa1, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xf6, 0x13, + 0xf3, 0x93, 0x5a, 0x37, 0xf3, 0x00, 0x90, 0x30, 0xe1, 0x34, 0x1c, 0x38, 0xf3, 0x40, 0xf6, 0x13, 0xf3, 0x04, 0x00, + 0x00, 0x00, 0x4c, 0x54, 0xdc, 0xe2, 0x08, 0x50, 0xdc, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x08, 0x50, 0xdc, 0xe2, 0x00, + 0x00, 0x00, 0x00, 0x38, 0xf6, 0x13, 0xf3, 0x2e, 0x05, 0x38, 0xf3, 0x08, 0x50, 0xdc, 0xe2, 0x2c, 0xf6, 0x13, 0xf3, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0a, 0x00, 0x01, 0x00, 0x00, 0x00, // Input capability set + 0x17, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // Rail capability set + 0x18, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // WindowList capability set + 0x00, 0x00, 0x00, 0x00, // session id +]; + +pub const CLIENT_DEMAND_ACTIVE_WITH_INCOMPLETE_CAPABILITY_SET_BUFFER: [u8; 501] = [ + 0xea, 0x3, // originator ID + 0x6, 0x0, // source descriptor length + 0xe9, 0x1, // combined length + 0x4d, 0x53, 0x54, 0x53, 0x43, 0x0, // source descriptor + 0x14, 0x0, // capability sets count + 0x0, 0x0, // padding + 0x1, 0x0, 0x18, 0x0, 0x1, 0x0, 0x3, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1d, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, // general capability set + 0x2, 0x0, 0x1c, 0x0, 0x20, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x20, 0x3, 0x58, 0x2, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, + 0x0, 0xa, 0x1, 0x0, 0x0, 0x0, // bitmap + 0x3, 0x0, 0x58, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x0, 0x14, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, + 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x84, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe4, 0x4, 0x0, 0x0, // order + 0x4, 0x0, 0x28, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x78, 0x0, 0x0, 0x4, 0x78, 0x0, 0x0, 0x10, 0x51, 0x1, 0x0, 0x40, // bitmap cache + 0xa, 0x0, 0x8, 0x0, 0x6, 0x0, 0x0, 0x0, // color cache + 0x7, 0x0, 0xc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // window activation + 0x5, 0x0, 0xc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x2, 0x0, // control + 0x8, 0x0, 0xa, 0x0, 0x1, 0x0, 0x14, 0x0, 0x15, 0x0, // pointer + 0x9, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, // share + 0xd, 0x0, 0x58, 0x0, 0x91, 0x0, 0x0, 0x0, 0x9, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // input + 0xc, 0x0, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0, // sound + 0xe, 0x0, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0, // font + 0x10, 0x0, 0x34, 0x0, 0xfe, 0x0, 0x4, 0x0, 0xfe, 0x0, 0x4, 0x0, 0xfe, 0x0, 0x8, 0x0, 0xfe, 0x0, 0x8, 0x0, 0xfe, + 0x0, 0x10, 0x0, 0xfe, 0x0, 0x20, 0x0, 0xfe, 0x0, 0x40, 0x0, 0xfe, 0x0, 0x80, 0x0, 0xfe, 0x0, 0x0, 0x1, 0x40, 0x0, + 0x0, 0x8, 0x0, 0x1, 0x0, 0x1, 0x3, 0x0, 0x0, 0x0, // glyph cache + 0xf, 0x0, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0, // brush + 0x11, 0x0, 0xc, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1e, 0x64, 0x0, // offscreen bitmap cache + 0x14, 0x0, 0x8, 0x0, 0x1, 0x0, 0x0, 0x0, // virtual channel + 0x15, 0x0, 0xc, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x1, // draw nine grid cache + 0x16, 0x0, 0x28, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // draw GDI plus + 0x1a, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, // multi fragment update + 0x18, 0x0, 0xb, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, // window list +]; + +pub const CLIENT_DEMAND_ACTIVE_BUFFER: [u8; 486] = [ + 0xea, 0x03, // originator ID + 0x06, 0x00, // source descriptor length + 0xda, 0x01, // combined length + 0x4d, 0x53, 0x54, 0x53, 0x43, 0x00, // source descriptor + 0x12, 0x00, // capabilities count + 0x00, 0x00, // padding + 0x01, 0x00, 0x18, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, // general capability set + 0x02, 0x00, 0x1c, 0x00, 0x18, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // bitmap capability set + 0x03, 0x00, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // order capability set + 0x13, 0x00, 0x28, 0x00, 0x03, 0x00, 0x00, 0x03, 0x78, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xfb, 0x09, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, // bitmap cache rev 2 capability set + 0x0a, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, // color cache + 0x07, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // window activation capability set + 0x05, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, // control capability set + 0x08, 0x00, 0x0a, 0x00, 0x01, 0x00, 0x14, 0x00, 0x15, 0x00, // pointer capability set + 0x09, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // share capability set + 0x0d, 0x00, 0x58, 0x00, 0x15, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // input capability set + 0x0c, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, // sound capability set + 0x0e, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, // font capability set + 0x10, 0x00, 0x34, 0x00, 0xfe, 0x00, 0x04, 0x00, 0xfe, 0x00, 0x04, 0x00, 0xfe, 0x00, 0x08, 0x00, 0xfe, 0x00, 0x08, + 0x00, 0xfe, 0x00, 0x10, 0x00, 0xfe, 0x00, 0x20, 0x00, 0xfe, 0x00, 0x40, 0x00, 0xfe, 0x00, 0x80, 0x00, 0xfe, 0x00, + 0x00, 0x01, 0x40, 0x00, 0x00, 0x08, 0x00, 0x01, 0x00, 0x01, 0x03, 0x00, 0x00, + 0x00, // glyph cache capability set + 0x0f, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, // brush capability set + 0x11, 0x00, 0x0c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x64, + 0x00, // offscreen bitmap cache capability set + 0x14, 0x00, 0x0c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00, // virtual channel capability set + 0x15, 0x00, 0x0c, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x01, // draw nine grid cache capability set + 0x16, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, // draw gdi plus capability set +]; + +pub const SERVER_SHARE_CAPABILITY_SET: [u8; 4] = [0xea, 0x03, 0xdc, 0xe2]; + +pub const SERVER_GENERAL_CAPABILITY_SET: [u8; 20] = [ + 0x01, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x01, +]; + +pub const SERVER_VIRTUAL_CHANNEL_CAPABILITY_SET: [u8; 8] = [0x02, 0x00, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00]; + +pub const SERVER_DRAW_GDI_PLUS_CAPABILITY_SET: [u8; 36] = [ + 0x00, 0x00, 0x00, 0x00, 0x70, 0xf6, 0x13, 0xf3, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x9c, 0xf6, 0x13, 0xf3, 0x61, 0xa6, 0x82, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x91, 0xbf, +]; + +pub const SERVER_FONT_CAPABILITY_SET: [u8; 4] = [0x00, 0x01, 0x00, 0x00]; + +pub const SERVER_BITMAP_CAPABILITY_SET: [u8; 24] = [ + 0x18, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, +]; + +pub const SERVER_ORDER_CAPABILITY_SET: [u8; 84] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x22, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, + 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub const SERVER_COLOR_CACHE_CAPABILITY_SET: [u8; 4] = [0x06, 0x00, 0x00, 0x00]; + +pub const SERVER_BITMAP_CACHE_HOST_SUPPORT_CAPABILITY_SET: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; + +pub const SERVER_POINTER_CAPABILITY_SET: [u8; 6] = [0x01, 0x00, 0x19, 0x00, 0x19, 0x00]; + +pub const SERVER_INPUT_CAPABILITY_SET: [u8; 84] = [ + 0x35, 0x00, 0x00, 0x00, 0xa1, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xf6, 0x13, 0xf3, 0x93, 0x5a, 0x37, + 0xf3, 0x00, 0x90, 0x30, 0xe1, 0x34, 0x1c, 0x38, 0xf3, 0x40, 0xf6, 0x13, 0xf3, 0x04, 0x00, 0x00, 0x00, 0x4c, 0x54, + 0xdc, 0xe2, 0x08, 0x50, 0xdc, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x08, 0x50, 0xdc, 0xe2, 0x00, 0x00, 0x00, 0x00, 0x38, + 0xf6, 0x13, 0xf3, 0x2e, 0x05, 0x38, 0xf3, 0x08, 0x50, 0xdc, 0xe2, 0x2c, 0xf6, 0x13, 0xf3, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x0a, 0x00, 0x01, 0x00, 0x00, 0x00, +]; +pub const SERVER_RAIL_CAPABILITY_SET: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; + +pub const SERVER_WINDOW_LIST_CAPABILITY_SET: [u8; 7] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + +pub const CLIENT_GENERAL_CAPABILITY_SET: [u8; 20] = [ + 0x01, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, +]; + +pub const CLIENT_BITMAP_CAPABILITY_SET: [u8; 24] = [ + 0x18, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, +]; + +pub const CLIENT_BITMAP_CAPABILITY_SET_32_BIT: [u8; 24] = [ + 0x20, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x20, 0x3, 0x58, 0x2, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0xa, 0x1, 0x0, + 0x0, 0x0, +]; + +pub const CLIENT_ORDER_CAPABILITY_SET: [u8; 84] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub const CLIENT_ORDER_CAPABILITY_SET_ANSI_CODE_PAGE: [u8; 84] = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x14, + 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, 0x1, 0x0, 0x1, 0x0, 0x0, + 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x84, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe4, 0x4, 0x0, 0x0, +]; + +pub const CLIENT_BITMAP_CACHE_REV_2_CAPABILITY_SET: [u8; 36] = [ + 0x03, 0x00, 0x00, 0x03, 0x78, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xfb, 0x09, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub const CLIENT_BITMAP_CACHE_REV_1_CAPABILITY_SET: [u8; 36] = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x78, 0x0, 0x0, 0x4, 0x78, 0x0, 0x0, 0x10, 0x51, 0x1, 0x0, 0x40, +]; + +pub const CLIENT_COLOR_CACHE_CAPABILITY_SET: [u8; 4] = [0x06, 0x00, 0x00, 0x00]; + +pub const CLIENT_WINDOW_ACTIVATION_CAPABILITY_SET: [u8; 8] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + +pub const CLIENT_CONTROL_CAPABILITY_SET: [u8; 8] = [0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00]; + +pub const CLIENT_POINTER_CAPABILITY_SET: [u8; 6] = [0x01, 0x00, 0x14, 0x00, 0x15, 0x00]; + +pub const CLIENT_SHARE_CAPABILITY_SET: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; + +pub const CLIENT_INPUT_CAPABILITY_SET: [u8; 84] = [ + 0x15, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub const CLIENT_INPUT_CAPABILITY_SET_UNICODE: [u8; 84] = [ + 0x91, 0x0, 0x20, 0x0, 0x9, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, +]; + +pub const CLIENT_SOUND_CAPABILITY_SET: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; + +pub const CLIENT_FONT_CAPABILITY_SET: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; + +pub const CLIENT_GLYPH_CACHE_CAPABILITY_SET: [u8; 48] = [ + 0xfe, 0x00, 0x04, 0x00, 0xfe, 0x00, 0x04, 0x00, 0xfe, 0x00, 0x08, 0x00, 0xfe, 0x00, 0x08, 0x00, 0xfe, 0x00, 0x10, + 0x00, 0xfe, 0x00, 0x20, 0x00, 0xfe, 0x00, 0x40, 0x00, 0xfe, 0x00, 0x80, 0x00, 0xfe, 0x00, 0x00, 0x01, 0x40, 0x00, + 0x00, 0x08, 0x00, 0x01, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, +]; + +pub const CLIENT_BRUSH_CAPABILITY_SET: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; + +pub const CLIENT_OFFSCREEN_BITMAP_CAPABILITY_SET: [u8; 8] = [0x01, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x64, 0x00]; + +pub const CLIENT_VIRTUAL_CHANNEL_CAPABILITY_SET_INCOMPLETE: [u8; 4] = [0x1, 0x0, 0x0, 0x0]; + +pub const CLIENT_VIRTUAL_CHANNEL_CAPABILITY_SET: [u8; 8] = [0x01, 0x00, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00]; + +pub const CLIENT_DRAW_NINE_GRID_CACHE_CAPABILITY_SET: [u8; 8] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x01]; + +pub const CLIENT_DRAW_GDI_PLUS_CAPABILITY_SET: [u8; 36] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub const CLIENT_MULTI_FRAGMENT_UPDATE_CAPABILITY_SET: [u8; 4] = [0x0, 0x0, 0x0, 0x0]; + +pub const CLIENT_WINDOW_LIST_CAPABILITY_SET: [u8; 7] = [0x1, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0]; + +pub static SERVER_DEMAND_ACTIVE: LazyLock = LazyLock::new(|| ServerDemandActive { + pdu: DemandActive { + source_descriptor: String::from("RDP"), + capability_sets: vec![ + CapabilitySet::Share(SERVER_SHARE_CAPABILITY_SET.to_vec()), + CapabilitySet::General(decode(SERVER_GENERAL_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::VirtualChannel(decode(SERVER_VIRTUAL_CHANNEL_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::DrawGdiPlus(SERVER_DRAW_GDI_PLUS_CAPABILITY_SET.to_vec()), + CapabilitySet::Font(SERVER_FONT_CAPABILITY_SET.to_vec()), + CapabilitySet::Bitmap(decode(SERVER_BITMAP_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Order(decode(SERVER_ORDER_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::ColorCache(SERVER_COLOR_CACHE_CAPABILITY_SET.to_vec()), + CapabilitySet::BitmapCacheHostSupport(SERVER_BITMAP_CACHE_HOST_SUPPORT_CAPABILITY_SET.to_vec()), + CapabilitySet::Pointer(decode(SERVER_POINTER_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Input(decode(SERVER_INPUT_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Rail(SERVER_RAIL_CAPABILITY_SET.to_vec()), + CapabilitySet::WindowList(SERVER_WINDOW_LIST_CAPABILITY_SET.to_vec()), + ], + }, +}); + +pub static CLIENT_DEMAND_ACTIVE_WITH_INCOMPLETE_CAPABILITY_SET: LazyLock = + LazyLock::new(|| ClientConfirmActive { + originator_id: SERVER_CHANNEL_ID, + pdu: DemandActive { + source_descriptor: String::from("MSTSC"), + capability_sets: vec![ + CapabilitySet::General(decode(CLIENT_GENERAL_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Bitmap(decode(CLIENT_BITMAP_CAPABILITY_SET_32_BIT.as_ref()).unwrap()), + CapabilitySet::Order(decode(CLIENT_ORDER_CAPABILITY_SET_ANSI_CODE_PAGE.as_ref()).unwrap()), + CapabilitySet::BitmapCache(decode(CLIENT_BITMAP_CACHE_REV_1_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::ColorCache(CLIENT_COLOR_CACHE_CAPABILITY_SET.to_vec()), + CapabilitySet::WindowActivation(CLIENT_WINDOW_ACTIVATION_CAPABILITY_SET.to_vec()), + CapabilitySet::Control(CLIENT_CONTROL_CAPABILITY_SET.to_vec()), + CapabilitySet::Pointer(decode(CLIENT_POINTER_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Share(CLIENT_SHARE_CAPABILITY_SET.to_vec()), + CapabilitySet::Input(decode(CLIENT_INPUT_CAPABILITY_SET_UNICODE.as_ref()).unwrap()), + CapabilitySet::Sound(decode(CLIENT_SOUND_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Font(CLIENT_FONT_CAPABILITY_SET.to_vec()), + CapabilitySet::GlyphCache(decode(CLIENT_GLYPH_CACHE_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Brush(decode(CLIENT_BRUSH_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::OffscreenBitmapCache(decode(CLIENT_OFFSCREEN_BITMAP_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::VirtualChannel( + decode(CLIENT_VIRTUAL_CHANNEL_CAPABILITY_SET_INCOMPLETE.as_ref()).unwrap(), + ), + CapabilitySet::DrawNineGridCache(CLIENT_DRAW_NINE_GRID_CACHE_CAPABILITY_SET.to_vec()), + CapabilitySet::DrawGdiPlus(CLIENT_DRAW_GDI_PLUS_CAPABILITY_SET.to_vec()), + CapabilitySet::MultiFragmentUpdate( + decode(CLIENT_MULTI_FRAGMENT_UPDATE_CAPABILITY_SET.as_ref()).unwrap(), + ), + CapabilitySet::WindowList(CLIENT_WINDOW_LIST_CAPABILITY_SET.to_vec()), + ], + }, + }); + +pub static CLIENT_DEMAND_ACTIVE: LazyLock = LazyLock::new(|| ClientConfirmActive { + originator_id: SERVER_CHANNEL_ID, + pdu: DemandActive { + source_descriptor: String::from("MSTSC"), + capability_sets: vec![ + CapabilitySet::General(decode(CLIENT_GENERAL_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Bitmap(decode(CLIENT_BITMAP_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Order(decode(CLIENT_ORDER_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::BitmapCacheRev2(decode(CLIENT_BITMAP_CACHE_REV_2_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::ColorCache(CLIENT_COLOR_CACHE_CAPABILITY_SET.to_vec()), + CapabilitySet::WindowActivation(CLIENT_WINDOW_ACTIVATION_CAPABILITY_SET.to_vec()), + CapabilitySet::Control(CLIENT_CONTROL_CAPABILITY_SET.to_vec()), + CapabilitySet::Pointer(decode(CLIENT_POINTER_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Share(CLIENT_SHARE_CAPABILITY_SET.to_vec()), + CapabilitySet::Input(decode(CLIENT_INPUT_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Sound(decode(CLIENT_SOUND_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Font(CLIENT_FONT_CAPABILITY_SET.to_vec()), + CapabilitySet::GlyphCache(decode(CLIENT_GLYPH_CACHE_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::Brush(decode(CLIENT_BRUSH_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::OffscreenBitmapCache(decode(CLIENT_OFFSCREEN_BITMAP_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::VirtualChannel(decode(CLIENT_VIRTUAL_CHANNEL_CAPABILITY_SET.as_ref()).unwrap()), + CapabilitySet::DrawNineGridCache(CLIENT_DRAW_NINE_GRID_CACHE_CAPABILITY_SET.to_vec()), + CapabilitySet::DrawGdiPlus(CLIENT_DRAW_GDI_PLUS_CAPABILITY_SET.to_vec()), + ], + }, +}); diff --git a/crates/ironrdp-testsuite-core/src/client_info.rs b/crates/ironrdp-testsuite-core/src/client_info.rs new file mode 100644 index 00000000..12c21b11 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/client_info.rs @@ -0,0 +1,155 @@ +use std::sync::LazyLock; + +use ironrdp_pdu::rdp::client_info::{ + AddressFamily, ClientInfo, ClientInfoFlags, CompressionType, Credentials, DayOfWeek, DayOfWeekOccurrence, + ExtendedClientInfo, ExtendedClientOptionalInfo, Month, OptionalSystemTime, PerformanceFlags, SystemTime, + TimezoneInfo, +}; + +pub const CLIENT_INFO_BUFFER_UNICODE_WITHOUT_OPTIONAL_FIELDS_LEN: usize = 218; + +pub const CLIENT_INFO_BUFFER_UNICODE: [u8; 398] = [ + 0x09, 0x04, 0x09, 0x04, // code page + 0xb3, 0x43, 0x00, 0x00, // flags + 0x0a, 0x00, // domain size + 0x0c, 0x00, // user name size + 0x00, 0x00, // password size + 0x00, 0x00, // alternate shell size + 0x00, 0x00, // work dir size + 0x4e, 0x00, 0x54, 0x00, 0x44, 0x00, 0x45, 0x00, 0x56, 0x00, 0x00, 0x00, // domain + 0x65, 0x00, 0x6c, 0x00, 0x74, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x73, 0x00, 0x00, 0x00, // user name + 0x00, 0x00, // password + 0x00, 0x00, // alternate shell + 0x00, 0x00, // work dir + 0x02, 0x00, // client address family + 0x1e, 0x00, // client address size + 0x31, 0x00, 0x35, 0x00, 0x37, 0x00, 0x2e, 0x00, 0x35, 0x00, 0x39, 0x00, 0x2e, 0x00, 0x32, 0x00, 0x34, 0x00, 0x32, + 0x00, 0x2e, 0x00, 0x31, 0x00, 0x35, 0x00, 0x36, 0x00, 0x00, 0x00, // client address + 0x84, 0x00, // client dir size + 0x43, 0x00, 0x3a, 0x00, 0x5c, 0x00, 0x64, 0x00, 0x65, 0x00, 0x70, 0x00, 0x6f, 0x00, 0x74, 0x00, 0x73, 0x00, 0x5c, + 0x00, 0x77, 0x00, 0x32, 0x00, 0x6b, 0x00, 0x33, 0x00, 0x5f, 0x00, 0x31, 0x00, 0x5c, 0x00, 0x74, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x6d, 0x00, 0x73, 0x00, 0x72, 0x00, 0x76, 0x00, 0x5c, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x77, 0x00, 0x63, + 0x00, 0x6c, 0x00, 0x69, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x5c, 0x00, 0x6c, 0x00, 0x69, 0x00, 0x62, 0x00, + 0x5c, 0x00, 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x33, 0x00, 0x32, 0x00, 0x5c, 0x00, 0x6f, 0x00, 0x62, 0x00, 0x6a, + 0x00, 0x5c, 0x00, 0x69, 0x00, 0x33, 0x00, 0x38, 0x00, 0x36, 0x00, 0x5c, 0x00, 0x6d, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x73, 0x00, 0x63, 0x00, 0x61, 0x00, 0x78, 0x00, 0x2e, 0x00, 0x64, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x00, + 0x00, // client dir + 0xe0, 0x01, 0x00, 0x00, 0x50, 0x00, 0x61, 0x00, 0x63, 0x00, 0x69, 0x00, 0x66, 0x00, 0x69, 0x00, 0x63, 0x00, 0x20, + 0x00, 0x53, 0x00, 0x74, 0x00, 0x61, 0x00, 0x6e, 0x00, 0x64, 0x00, 0x61, 0x00, 0x72, 0x00, 0x64, 0x00, 0x20, 0x00, + 0x54, 0x00, 0x69, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x05, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x61, 0x00, 0x63, 0x00, 0x69, + 0x00, 0x66, 0x00, 0x69, 0x00, 0x63, 0x00, 0x20, 0x00, 0x44, 0x00, 0x61, 0x00, 0x79, 0x00, 0x6c, 0x00, 0x69, 0x00, + 0x67, 0x00, 0x68, 0x00, 0x74, 0x00, 0x20, 0x00, 0x54, 0x00, 0x69, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc4, 0xff, 0xff, + 0xff, // TimezoneInfo + 0x00, 0x00, 0x00, 0x00, // session id + 0x01, 0x00, 0x00, 0x00, // performance flags +]; + +pub const CLIENT_INFO_BUFFER_ANSI: [u8; 301] = [ + 0x09, 0x04, 0x09, 0x04, // code page + 0xa3, 0x43, 0x00, 0x00, // flags + 0x05, 0x00, // domain size + 0x06, 0x00, // user name size + 0x00, 0x00, // password size + 0x00, 0x00, // alternate shell size + 0x00, 0x00, // work dir size + 0x4e, 0x54, 0x44, 0x45, 0x56, 0x00, // domain + 0x65, 0x6c, 0x74, 0x6f, 0x6e, 0x73, 0x00, // user name + 0x00, // password + 0x00, // alternate shell + 0x00, // work dir + 0x02, 0x00, // client address family + 0x0f, 0x00, // client address size + 0x31, 0x35, 0x37, 0x2e, 0x35, 0x39, 0x2e, 0x32, 0x34, 0x32, 0x2e, 0x31, 0x35, 0x36, 0x00, // client address + 0x42, 0x00, // client dir size + 0x43, 0x3a, 0x5c, 0x64, 0x65, 0x70, 0x6f, 0x74, 0x73, 0x5c, 0x77, 0x32, 0x6b, 0x33, 0x5f, 0x31, 0x5c, 0x74, 0x65, + 0x72, 0x6d, 0x73, 0x72, 0x76, 0x5c, 0x6e, 0x65, 0x77, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5c, 0x6c, 0x69, 0x62, + 0x5c, 0x77, 0x69, 0x6e, 0x33, 0x32, 0x5c, 0x6f, 0x62, 0x6a, 0x5c, 0x69, 0x33, 0x38, 0x36, 0x5c, 0x6d, 0x73, 0x74, + 0x73, 0x63, 0x61, 0x78, 0x2e, 0x64, 0x6c, 0x6c, 0x00, // client dir + 0xe0, 0x01, 0x00, 0x00, 0x50, 0x00, 0x61, 0x00, 0x63, 0x00, 0x69, 0x00, 0x66, 0x00, 0x69, 0x00, 0x63, 0x00, 0x20, + 0x00, 0x53, 0x00, 0x74, 0x00, 0x61, 0x00, 0x6e, 0x00, 0x64, 0x00, 0x61, 0x00, 0x72, 0x00, 0x64, 0x00, 0x20, 0x00, + 0x54, 0x00, 0x69, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x05, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x61, 0x00, 0x63, 0x00, 0x69, + 0x00, 0x66, 0x00, 0x69, 0x00, 0x63, 0x00, 0x20, 0x00, 0x44, 0x00, 0x61, 0x00, 0x79, 0x00, 0x6c, 0x00, 0x69, 0x00, + 0x67, 0x00, 0x68, 0x00, 0x74, 0x00, 0x20, 0x00, 0x54, 0x00, 0x69, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc4, 0xff, 0xff, + 0xff, // TimezoneInfo + 0x00, 0x00, 0x00, 0x00, // session id + 0x01, 0x00, 0x00, 0x00, // performance flags +]; + +pub static CLIENT_INFO_UNICODE: LazyLock = LazyLock::new(|| ClientInfo { + code_page: 0x0409_0409, + flags: ClientInfoFlags::MOUSE + | ClientInfoFlags::DISABLE_CTRL_ALT_DEL + | ClientInfoFlags::UNICODE + | ClientInfoFlags::MAXIMIZE_SHELL + | ClientInfoFlags::COMPRESSION + | ClientInfoFlags::ENABLE_WINDOWS_KEY + | ClientInfoFlags::FORCE_ENCRYPTED_CS_PDU, + compression_type: CompressionType::K64, + credentials: Credentials { + username: String::from("eltons"), + password: String::from(""), + domain: Some(String::from("NTDEV")), + }, + alternate_shell: String::from(""), + work_dir: String::from(""), + extra_info: ExtendedClientInfo { + address_family: AddressFamily::INET, + address: String::from("157.59.242.156"), + dir: String::from("C:\\depots\\w2k3_1\\termsrv\\newclient\\lib\\win32\\obj\\i386\\mstscax.dll"), + optional_data: ExtendedClientOptionalInfo::builder() + .timezone(TimezoneInfo { + bias: 480, + standard_name: String::from("Pacific Standard Time"), + standard_date: OptionalSystemTime(Some(SystemTime { + month: Month::October, + day_of_week: DayOfWeek::Sunday, + day: DayOfWeekOccurrence::Last, + hour: 2, + minute: 0, + second: 0, + milliseconds: 0, + })), + standard_bias: 0, + daylight_name: String::from("Pacific Daylight Time"), + daylight_date: OptionalSystemTime(Some(SystemTime { + month: Month::April, + day_of_week: DayOfWeek::Sunday, + day: DayOfWeekOccurrence::First, + hour: 2, + minute: 0, + second: 0, + milliseconds: 0, + })), + daylight_bias: -60, + }) + .session_id(0) + .performance_flags(PerformanceFlags::DISABLE_WALLPAPER) + .build(), + }, +}); + +pub static CLIENT_INFO_ANSI: LazyLock = LazyLock::new(|| { + let mut client_info = CLIENT_INFO_UNICODE.clone(); + client_info.flags -= ClientInfoFlags::UNICODE; + client_info +}); + +pub static CLIENT_INFO_UNICODE_WITHOUT_OPTIONAL_FIELDS: LazyLock = LazyLock::new(|| { + let mut client_info = CLIENT_INFO_UNICODE.clone(); + client_info.extra_info.optional_data = ExtendedClientOptionalInfo::default(); + client_info +}); + +pub static CLIENT_INFO_BUFFER_UNICODE_WITHOUT_OPTIONAL_FIELDS: LazyLock> = LazyLock::new(|| { + let mut buffer = CLIENT_INFO_BUFFER_UNICODE.to_vec(); + buffer.truncate(CLIENT_INFO_BUFFER_UNICODE_WITHOUT_OPTIONAL_FIELDS_LEN); + buffer +}); diff --git a/crates/ironrdp-testsuite-core/src/cluster_data.rs b/crates/ironrdp-testsuite-core/src/cluster_data.rs new file mode 100644 index 00000000..165430a9 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/cluster_data.rs @@ -0,0 +1,11 @@ +use std::sync::LazyLock; + +use ironrdp_pdu::gcc::{ClientClusterData, RedirectionFlags, RedirectionVersion}; + +pub const CLUSTER_DATA_BUFFER: [u8; 8] = [0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + +pub static CLUSTER_DATA: LazyLock = LazyLock::new(|| ClientClusterData { + flags: RedirectionFlags::REDIRECTION_SUPPORTED, + redirection_version: RedirectionVersion::V4, + redirected_session_id: 0, +}); diff --git a/crates/ironrdp-testsuite-core/src/conference_create.rs b/crates/ironrdp-testsuite-core/src/conference_create.rs new file mode 100644 index 00000000..92ab3344 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/conference_create.rs @@ -0,0 +1,39 @@ +use std::sync::LazyLock; + +use array_concat::{concat_arrays, concat_arrays_size}; +use ironrdp_pdu::gcc::{ConferenceCreateRequest, ConferenceCreateResponse}; + +use crate::gcc; + +pub const CONFERENCE_CREATE_REQUEST_PREFIX_BUFFER: [u8; 23] = [ + 0x00, 0x05, 0x00, 0x14, 0x7c, 0x00, 0x01, 0x81, 0x28, 0x00, 0x08, 0x00, 0x10, 0x00, 0x01, 0xc0, 0x00, 0x44, 0x75, + 0x63, 0x61, 0x81, 0x1c, +]; + +pub const CONFERENCE_CREATE_RESPONSE_PREFIX_BUFFER: [u8; 24] = [ + 0x00, 0x05, 0x00, 0x14, 0x7c, 0x00, 0x01, 0x81, 0x16, 0x14, 0x76, 0x0a, 0x01, 0x01, 0x00, 0x01, 0xc0, 0x00, 0x4d, + 0x63, 0x44, 0x6e, 0x81, 0x08, +]; + +pub static CONFERENCE_CREATE_REQUEST: LazyLock = LazyLock::new(|| { + ConferenceCreateRequest::new(gcc::CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD.clone()).expect("should not fail") +}); +pub static CONFERENCE_CREATE_RESPONSE: LazyLock = LazyLock::new(|| { + ConferenceCreateResponse::new(0x79f3, gcc::SERVER_GCC_WITHOUT_OPTIONAL_FIELDS.clone()).expect("should not fail") +}); + +pub const CONFERENCE_CREATE_REQUEST_BUFFER: [u8; concat_arrays_size!( + CONFERENCE_CREATE_REQUEST_PREFIX_BUFFER, + gcc::CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD_BUFFER +)] = concat_arrays!( + CONFERENCE_CREATE_REQUEST_PREFIX_BUFFER, + gcc::CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD_BUFFER +); + +pub const CONFERENCE_CREATE_RESPONSE_BUFFER: [u8; concat_arrays_size!( + CONFERENCE_CREATE_RESPONSE_PREFIX_BUFFER, + gcc::SERVER_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER +)] = concat_arrays!( + CONFERENCE_CREATE_RESPONSE_PREFIX_BUFFER, + gcc::SERVER_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER +); diff --git a/crates/ironrdp-testsuite-core/src/core_data.rs b/crates/ironrdp-testsuite-core/src/core_data.rs new file mode 100644 index 00000000..78a08d8f --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/core_data.rs @@ -0,0 +1,196 @@ +use std::sync::LazyLock; + +use array_concat::{concat_arrays, concat_arrays_size}; +use ironrdp_pdu::gcc::{ + ClientCoreData, ClientCoreOptionalData, ClientEarlyCapabilityFlags, ColorDepth, ConnectionType, HighColorDepth, + KeyboardType, RdpVersion, SecureAccessSequence, ServerCoreData, ServerCoreOptionalData, ServerEarlyCapabilityFlags, + SupportedColorDepths, +}; +use ironrdp_pdu::nego::SecurityProtocol; + +pub const CLIENT_CORE_DATA_BUFFER: [u8; 128] = [ + 0x04, 0x00, 0x08, 0x00, // version + 0x00, 0x05, // desktop width + 0x00, 0x04, // desktop height + 0x00, 0xca, // color depth + 0x03, 0xaa, // sas sequence + 0x09, 0x04, 0x00, 0x00, // keyboard layout + 0xce, 0x0e, 0x00, 0x00, // client build + 0x45, 0x00, 0x4c, 0x00, 0x54, 0x00, 0x4f, 0x00, 0x4e, 0x00, 0x53, 0x00, 0x2d, 0x00, 0x44, 0x00, 0x45, 0x00, 0x56, + 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // client name + 0x04, 0x00, 0x00, 0x00, // keyboard type + 0x00, 0x00, 0x00, 0x00, // keyboard subtype + 0x0c, 0x00, 0x00, 0x00, // keyboard function key + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ime file name +]; + +pub const CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH_BUFFER: [u8; 8] = [ + 0x01, 0xca, // post beta color depth + 0x01, 0x00, // client product id + 0x00, 0x00, 0x00, 0x00, // serial number +]; + +pub const EARLY_CAPABILITY_FLAGS_START: usize = 4; + +pub const EARLY_CAPABILITY_FLAGS_LENGTH: usize = 2; + +pub const CLIENT_OPTIONAL_CORE_DATA_FROM_HIGH_COLOR_DEPTH_TO_SERVER_SELECTED_PROTOCOL_BUFFER: [u8; 76] = [ + 0x18, 0x00, // high color depth + 0x07, 0x00, // supported color depths + 0x01, 0x00, // early capability flags + 0x36, 0x00, 0x39, 0x00, 0x37, 0x00, 0x31, 0x00, 0x32, 0x00, 0x2d, 0x00, 0x37, 0x00, 0x38, 0x00, 0x33, 0x00, 0x2d, + 0x00, 0x30, 0x00, 0x33, 0x00, 0x35, 0x00, 0x37, 0x00, 0x39, 0x00, 0x37, 0x00, 0x34, 0x00, 0x2d, 0x00, 0x34, 0x00, + 0x32, 0x00, 0x37, 0x00, 0x31, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // client dig product id + 0x00, // connection type + 0x00, // padding + 0x00, 0x00, 0x00, 0x00, // server selected protocol +]; + +pub const CLIENT_OPTIONAL_CORE_DATA_FROM_DESKTOP_PHYSICAL_WIDTH_TO_DEVICE_SCALE_FACTOR_BUFFER: [u8; 18] = [ + 0x88, 0x13, 0x00, 0x00, // desktop physical width + 0xb8, 0x0b, 0x00, 0x00, //desktop physical height + 0x5a, 0x00, // desktop orientation + 0xc8, 0x00, 0x00, 0x00, // desktop scale factor + 0x8c, 0x00, 0x00, 0x00, // device scale factor +]; + +pub static CLIENT_CORE_DATA_WITHOUT_OPTIONAL_FIELDS: LazyLock = LazyLock::new(|| ClientCoreData { + version: RdpVersion::V5_PLUS, + desktop_width: 1280, + desktop_height: 1024, + color_depth: ColorDepth::Bpp4, + sec_access_sequence: SecureAccessSequence::Del, + keyboard_layout: 1033, + client_build: 3790, + client_name: String::from("ELTONS-DEV2"), + keyboard_type: KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_functional_keys_count: 12, + ime_file_name: String::new(), + optional_data: ClientCoreOptionalData::default(), +}); + +pub static CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH: LazyLock = LazyLock::new(|| { + let mut data = CLIENT_CORE_DATA_WITHOUT_OPTIONAL_FIELDS.clone(); + data.optional_data.post_beta2_color_depth = Some(ColorDepth::Bpp8); + data.optional_data.client_product_id = Some(1); + data.optional_data.serial_number = Some(0); + data +}); + +pub static CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL: LazyLock = LazyLock::new(|| { + let mut data = CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH.clone(); + data.optional_data.high_color_depth = Some(HighColorDepth::Bpp24); + data.optional_data.supported_color_depths = + Some(SupportedColorDepths::BPP24 | SupportedColorDepths::BPP16 | SupportedColorDepths::BPP15); + data.optional_data.early_capability_flags = Some(ClientEarlyCapabilityFlags::SUPPORT_ERR_INFO_PDU); + data.optional_data.dig_product_id = Some(String::from("69712-783-0357974-42714")); + data.optional_data.connection_type = Some(ConnectionType::NotUsed); + data.optional_data.server_selected_protocol = Some(SecurityProtocol::empty()); + data +}); + +pub static CLIENT_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS: LazyLock = LazyLock::new(|| { + let mut data = CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL.clone(); + data.optional_data.desktop_physical_width = Some(5000); + data.optional_data.desktop_physical_height = Some(3000); + data.optional_data.desktop_orientation = Some(90); + data.optional_data.desktop_scale_factor = Some(200); + data.optional_data.device_scale_factor = Some(140); + data +}); +pub static CLIENT_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_WITH_WANT_32_BPP_EARLY_FLAG: LazyLock = + LazyLock::new(|| { + let mut data = CLIENT_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS.clone(); + data.optional_data.early_capability_flags = Some(ClientEarlyCapabilityFlags::WANT_32_BPP_SESSION); + data + }); +pub static CLIENT_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_WITH_WANT_32_BPP_EARLY_FLAG_BUFFER: LazyLock> = + LazyLock::new(|| { + let early_capability_flags = ClientEarlyCapabilityFlags::WANT_32_BPP_SESSION.bits().to_le_bytes(); + + let mut from_high_color_to_server_protocol = + CLIENT_OPTIONAL_CORE_DATA_FROM_HIGH_COLOR_DEPTH_TO_SERVER_SELECTED_PROTOCOL_BUFFER; + from_high_color_to_server_protocol + [EARLY_CAPABILITY_FLAGS_START..EARLY_CAPABILITY_FLAGS_START + EARLY_CAPABILITY_FLAGS_LENGTH] + .clone_from_slice(early_capability_flags.as_ref()); + + let mut buffer = CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH_BUFFER_BUFFER.to_vec(); + buffer.extend(from_high_color_to_server_protocol.as_ref()); + buffer.extend(CLIENT_OPTIONAL_CORE_DATA_FROM_DESKTOP_PHYSICAL_WIDTH_TO_DEVICE_SCALE_FACTOR_BUFFER.as_ref()); + + buffer + }); + +pub const CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH_BUFFER_BUFFER: [u8; concat_arrays_size!( + CLIENT_CORE_DATA_BUFFER, + CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH_BUFFER +)] = concat_arrays!( + CLIENT_CORE_DATA_BUFFER, + CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH_BUFFER +); + +pub const CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL_BUFFER: [u8; concat_arrays_size!( + CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH_BUFFER_BUFFER, + CLIENT_OPTIONAL_CORE_DATA_FROM_HIGH_COLOR_DEPTH_TO_SERVER_SELECTED_PROTOCOL_BUFFER +)] = concat_arrays!( + CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH_BUFFER_BUFFER, + CLIENT_OPTIONAL_CORE_DATA_FROM_HIGH_COLOR_DEPTH_TO_SERVER_SELECTED_PROTOCOL_BUFFER +); + +pub const CLIENT_OPTIONAL_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_BUFFER: [u8; concat_arrays_size!( + CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL_BUFFER, + CLIENT_OPTIONAL_CORE_DATA_FROM_DESKTOP_PHYSICAL_WIDTH_TO_DEVICE_SCALE_FACTOR_BUFFER +)] = concat_arrays!( + CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL_BUFFER, + CLIENT_OPTIONAL_CORE_DATA_FROM_DESKTOP_PHYSICAL_WIDTH_TO_DEVICE_SCALE_FACTOR_BUFFER +); + +pub const SERVER_CORE_DATA_BUFFER: [u8; 4] = [ + 0x04, 0x00, 0x08, 0x00, // version +]; + +pub const REQUESTED_PROTOCOL_BUFFER: [u8; 4] = [ + 0x00, 0x00, 0x00, 0x00, // client requested protocols +]; + +pub const FLAGS_BUFFER: [u8; 4] = [ + 0x01, 0x00, 0x00, 0x00, // early capability flags +]; + +pub static SERVER_CORE_DATA: LazyLock = LazyLock::new(|| ServerCoreData { + version: RdpVersion::V5_PLUS, + optional_data: ServerCoreOptionalData { + client_requested_protocols: None, + early_capability_flags: None, + }, +}); +pub static SERVER_CORE_DATA_TO_FLAGS: LazyLock = LazyLock::new(|| ServerCoreData { + version: RdpVersion::V5_PLUS, + optional_data: ServerCoreOptionalData { + client_requested_protocols: Some(SecurityProtocol::empty()), + early_capability_flags: None, + }, +}); + +pub static SERVER_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS: LazyLock = LazyLock::new(|| ServerCoreData { + version: RdpVersion::V5_PLUS, + optional_data: ServerCoreOptionalData { + client_requested_protocols: Some(SecurityProtocol::empty()), + early_capability_flags: Some(ServerEarlyCapabilityFlags::EDGE_ACTIONS_SUPPORTED_V1), + }, +}); + +pub const SERVER_CORE_DATA_TO_REQUESTED_PROTOCOL_BUFFER: [u8; concat_arrays_size!( + SERVER_CORE_DATA_BUFFER, + REQUESTED_PROTOCOL_BUFFER +)] = concat_arrays!(SERVER_CORE_DATA_BUFFER, REQUESTED_PROTOCOL_BUFFER); + +pub const SERVER_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_BUFFER: [u8; concat_arrays_size!( + SERVER_CORE_DATA_TO_REQUESTED_PROTOCOL_BUFFER, + FLAGS_BUFFER +)] = concat_arrays!(SERVER_CORE_DATA_TO_REQUESTED_PROTOCOL_BUFFER, FLAGS_BUFFER); diff --git a/crates/ironrdp-testsuite-core/src/gcc.rs b/crates/ironrdp-testsuite-core/src/gcc.rs new file mode 100644 index 00000000..1ebe270e --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/gcc.rs @@ -0,0 +1,239 @@ +use std::sync::LazyLock; + +use array_concat::{concat_arrays, concat_arrays_size}; +use ironrdp_pdu::gcc::{ClientGccBlocks, ClientGccType, ServerGccBlocks, ServerGccType}; + +use crate::cluster_data::{CLUSTER_DATA, CLUSTER_DATA_BUFFER}; +use crate::core_data::{ + CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL, + CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL_BUFFER, SERVER_CORE_DATA_TO_FLAGS, + SERVER_CORE_DATA_TO_REQUESTED_PROTOCOL_BUFFER, +}; +use crate::message_channel_data::SERVER_GCC_MESSAGE_CHANNEL_BLOCK; +use crate::multi_transport_channel_data::SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK; +use crate::network_data::{ + CLIENT_NETWORK_DATA_WITH_CHANNELS, CLIENT_NETWORK_DATA_WITH_CHANNELS_BUFFER, SERVER_NETWORK_DATA_WITH_CHANNELS_ID, + SERVER_NETWORK_DATA_WITH_CHANNELS_ID_BUFFER, +}; +use crate::security_data::{ + CLIENT_SECURITY_DATA, CLIENT_SECURITY_DATA_BUFFER, SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS, + SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_BUFFER, +}; + +const USER_HEADER_LEN: usize = 4; + +const fn gcc_block_size(_: [u8; N]) -> usize { + N + USER_HEADER_LEN +} + +const fn make_gcc_block_buffer(data_type: u16, buffer: &[u8]) -> [u8; N] { + const fn copy_slice(src: &[u8], mut dst: [u8; N], offset: usize) -> [u8; N] { + let mut i = src.len(); + while i > 0 { + i -= 1; + dst[i + offset] = src[i]; + } + dst + } + + if N != buffer.len() + USER_HEADER_LEN { + panic!("invalid output array len"); + } + + let array = copy_slice(&data_type.to_le_bytes(), [0; N], 0); + + #[expect(clippy::as_conversions, reason = "must be const casts")] + let length = (buffer.len() + USER_HEADER_LEN) as u16; + let array = copy_slice(&length.to_le_bytes(), array, 2); + + copy_slice(buffer, array, 4) +} + +pub const CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER: [u8; concat_arrays_size!( + CLIENT_GCC_CORE_BLOCK_BUFFER, + CLIENT_GCC_SECURITY_BLOCK_BUFFER, + CLIENT_GCC_NETWORK_BLOCK_BUFFER +)] = concat_arrays!( + CLIENT_GCC_CORE_BLOCK_BUFFER, + CLIENT_GCC_SECURITY_BLOCK_BUFFER, + CLIENT_GCC_NETWORK_BLOCK_BUFFER +); + +pub const CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD_BUFFER: [u8; concat_arrays_size!( + CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER, + CLIENT_GCC_CLUSTER_BLOCK_BUFFER +)] = concat_arrays!( + CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER, + CLIENT_GCC_CLUSTER_BLOCK_BUFFER +); + +pub const CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS_BUFFER: [u8; concat_arrays_size!( + CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD_BUFFER, + CLIENT_GCC_MONITOR_BLOCK_BUFFER, + CLIENT_GCC_MONITOR_EXTENDED_BLOCK_BUFFER +)] = concat_arrays!( + CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD_BUFFER, + CLIENT_GCC_MONITOR_BLOCK_BUFFER, + CLIENT_GCC_MONITOR_EXTENDED_BLOCK_BUFFER +); + +pub const CLIENT_GCC_WITH_OPTIONAL_FIELDS_IN_DIFFERENT_ORDER_BUFFER: [u8; concat_arrays_size!( + CLIENT_GCC_CORE_BLOCK_BUFFER, + CLIENT_GCC_CLUSTER_BLOCK_BUFFER, + CLIENT_GCC_SECURITY_BLOCK_BUFFER, + CLIENT_GCC_MONITOR_BLOCK_BUFFER, + CLIENT_GCC_NETWORK_BLOCK_BUFFER, + CLIENT_GCC_MONITOR_EXTENDED_BLOCK_BUFFER +)] = concat_arrays!( + CLIENT_GCC_CORE_BLOCK_BUFFER, + CLIENT_GCC_CLUSTER_BLOCK_BUFFER, + CLIENT_GCC_SECURITY_BLOCK_BUFFER, + CLIENT_GCC_MONITOR_BLOCK_BUFFER, + CLIENT_GCC_NETWORK_BLOCK_BUFFER, + CLIENT_GCC_MONITOR_EXTENDED_BLOCK_BUFFER +); + +pub const SERVER_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER: [u8; concat_arrays_size!( + SERVER_GCC_CORE_BLOCK_BUFFER, + SERVER_GCC_NETWORK_BLOCK_BUFFER, + SERVER_GCC_SECURITY_BLOCK_BUFFER +)] = concat_arrays!( + SERVER_GCC_CORE_BLOCK_BUFFER, + SERVER_GCC_NETWORK_BLOCK_BUFFER, + SERVER_GCC_SECURITY_BLOCK_BUFFER +); + +pub const SERVER_GCC_WITH_OPTIONAL_FIELDS_BUFFER: [u8; concat_arrays_size!( + SERVER_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER, + SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER, + SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER +)] = concat_arrays!( + SERVER_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER, + SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER, + SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER +); + +pub const SERVER_GCC_WITH_OPTIONAL_FIELDS_IN_DIFFERENT_ORDER_BUFFER: [u8; concat_arrays_size!( + SERVER_GCC_CORE_BLOCK_BUFFER, + SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER, + SERVER_GCC_NETWORK_BLOCK_BUFFER, + SERVER_GCC_SECURITY_BLOCK_BUFFER, + SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER +)] = concat_arrays!( + SERVER_GCC_CORE_BLOCK_BUFFER, + SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER, + SERVER_GCC_NETWORK_BLOCK_BUFFER, + SERVER_GCC_SECURITY_BLOCK_BUFFER, + SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER +); + +pub static CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS: LazyLock = LazyLock::new(|| ClientGccBlocks { + core: CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL.clone(), + security: CLIENT_SECURITY_DATA.clone(), + network: Some(CLIENT_NETWORK_DATA_WITH_CHANNELS.clone()), + cluster: None, + monitor: None, + message_channel: None, + multi_transport_channel: None, + monitor_extended: None, +}); +pub static CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD: LazyLock = LazyLock::new(|| { + let mut data = CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS.clone(); + data.cluster = Some(CLUSTER_DATA.clone()); + data +}); +pub static CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS: LazyLock = LazyLock::new(|| { + let mut data = CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD.clone(); + data.monitor = Some(crate::monitor_data::MONITOR_DATA_WITH_MONITORS.clone()); + data.monitor_extended = Some(crate::monitor_extended_data::MONITOR_DATA_WITH_MONITORS.clone()); + data +}); +pub static SERVER_GCC_WITHOUT_OPTIONAL_FIELDS: LazyLock = LazyLock::new(|| ServerGccBlocks { + core: SERVER_CORE_DATA_TO_FLAGS.clone(), + network: SERVER_NETWORK_DATA_WITH_CHANNELS_ID.clone(), + security: SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS.clone(), + message_channel: None, + multi_transport_channel: None, +}); +pub static SERVER_GCC_WITH_OPTIONAL_FIELDS: LazyLock = LazyLock::new(|| { + let mut data = SERVER_GCC_WITHOUT_OPTIONAL_FIELDS.clone(); + data.message_channel = Some(SERVER_GCC_MESSAGE_CHANNEL_BLOCK.clone()); + data.multi_transport_channel = Some(SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK.clone()); + data +}); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const CLIENT_GCC_CORE_BLOCK_BUFFER: [u8; gcc_block_size( + CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL_BUFFER, +)] = make_gcc_block_buffer( + ClientGccType::CoreData as u16, + &CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL_BUFFER, +); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const CLIENT_GCC_SECURITY_BLOCK_BUFFER: [u8; gcc_block_size(CLIENT_SECURITY_DATA_BUFFER)] = + make_gcc_block_buffer(ClientGccType::SecurityData as u16, &CLIENT_SECURITY_DATA_BUFFER); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const CLIENT_GCC_NETWORK_BLOCK_BUFFER: [u8; gcc_block_size(CLIENT_NETWORK_DATA_WITH_CHANNELS_BUFFER)] = + make_gcc_block_buffer( + ClientGccType::NetworkData as u16, + &CLIENT_NETWORK_DATA_WITH_CHANNELS_BUFFER, + ); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const CLIENT_GCC_CLUSTER_BLOCK_BUFFER: [u8; gcc_block_size(CLUSTER_DATA_BUFFER)] = + make_gcc_block_buffer(ClientGccType::ClusterData as u16, &CLUSTER_DATA_BUFFER); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const CLIENT_GCC_MONITOR_BLOCK_BUFFER: [u8; gcc_block_size( + crate::monitor_data::MONITOR_DATA_WITH_MONITORS_BUFFER, +)] = make_gcc_block_buffer( + ClientGccType::MonitorData as u16, + &crate::monitor_data::MONITOR_DATA_WITH_MONITORS_BUFFER, +); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const CLIENT_GCC_MONITOR_EXTENDED_BLOCK_BUFFER: [u8; gcc_block_size( + crate::monitor_extended_data::MONITOR_DATA_WITH_MONITORS_BUFFER, +)] = make_gcc_block_buffer( + ClientGccType::MonitorExtendedData as u16, + &crate::monitor_extended_data::MONITOR_DATA_WITH_MONITORS_BUFFER, +); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const SERVER_GCC_CORE_BLOCK_BUFFER: [u8; gcc_block_size(SERVER_CORE_DATA_TO_REQUESTED_PROTOCOL_BUFFER)] = + make_gcc_block_buffer( + ServerGccType::CoreData as u16, + &SERVER_CORE_DATA_TO_REQUESTED_PROTOCOL_BUFFER, + ); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const SERVER_GCC_NETWORK_BLOCK_BUFFER: [u8; gcc_block_size(SERVER_NETWORK_DATA_WITH_CHANNELS_ID_BUFFER)] = + make_gcc_block_buffer( + ServerGccType::NetworkData as u16, + &SERVER_NETWORK_DATA_WITH_CHANNELS_ID_BUFFER, + ); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const SERVER_GCC_SECURITY_BLOCK_BUFFER: [u8; gcc_block_size(SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_BUFFER)] = + make_gcc_block_buffer( + ServerGccType::SecurityData as u16, + &SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_BUFFER, + ); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER: [u8; gcc_block_size( + crate::message_channel_data::SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER, +)] = make_gcc_block_buffer( + ServerGccType::MessageChannelData as u16, + &crate::message_channel_data::SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER, +); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER: [u8; gcc_block_size( + crate::multi_transport_channel_data::SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER, +)] = make_gcc_block_buffer( + ServerGccType::MultiTransportChannelData as u16, + &crate::multi_transport_channel_data::SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER, +); diff --git a/crates/ironrdp-testsuite-core/src/gfx.rs b/crates/ironrdp-testsuite-core/src/gfx.rs new file mode 100644 index 00000000..b9616048 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/gfx.rs @@ -0,0 +1,19 @@ +use std::sync::LazyLock; + +use ironrdp_pdu::rdp::vc::dvc::gfx::{ClientPdu, ServerPdu}; + +use crate::graphics_messages::{ + FRAME_ACKNOWLEDGE, FRAME_ACKNOWLEDGE_BUFFER, WIRE_TO_SURFACE_1, WIRE_TO_SURFACE_1_BUFFER, +}; + +pub const WIRE_TO_SURFACE_1_HEADER_BUFFER: [u8; 8] = [0x01, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00]; +pub const FRAME_ACKNOWLEDGE_HEADER_BUFFER: [u8; 8] = [0x0d, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00]; + +pub static HEADER_WITH_WIRE_TO_SURFACE_1_BUFFER: LazyLock> = + LazyLock::new(|| [&WIRE_TO_SURFACE_1_HEADER_BUFFER[..], &WIRE_TO_SURFACE_1_BUFFER[..]].concat()); +pub static HEADER_WITH_FRAME_ACKNOWLEDGE_BUFFER: LazyLock> = + LazyLock::new(|| [&FRAME_ACKNOWLEDGE_HEADER_BUFFER[..], &FRAME_ACKNOWLEDGE_BUFFER[..]].concat()); +pub static HEADER_WITH_WIRE_TO_SURFACE_1: LazyLock = + LazyLock::new(|| ServerPdu::WireToSurface1(WIRE_TO_SURFACE_1.clone())); +pub static HEADER_WITH_FRAME_ACKNOWLEDGE: LazyLock = + LazyLock::new(|| ClientPdu::FrameAcknowledge(FRAME_ACKNOWLEDGE.clone())); diff --git a/crates/ironrdp-testsuite-core/src/graphics_messages.rs b/crates/ironrdp-testsuite-core/src/graphics_messages.rs new file mode 100644 index 00000000..ee0b4f0d --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/graphics_messages.rs @@ -0,0 +1,455 @@ +use std::sync::LazyLock; + +use ironrdp_pdu::gcc::{Monitor, MonitorFlags}; +use ironrdp_pdu::geometry::InclusiveRectangle; +use ironrdp_pdu::rdp::vc::dvc::gfx::{ + Avc420BitmapStream, Avc444BitmapStream, CacheImportReplyPdu, CacheToSurfacePdu, CapabilitiesAdvertisePdu, + CapabilitiesConfirmPdu, CapabilitiesV103Flags, CapabilitiesV104Flags, CapabilitiesV10Flags, CapabilitiesV81Flags, + CapabilitiesV8Flags, CapabilitySet, Codec1Type, Codec2Type, Color, CreateSurfacePdu, DeleteEncodingContextPdu, + DeleteSurfacePdu, Encoding, EndFramePdu, EvictCacheEntryPdu, FrameAcknowledgePdu, MapSurfaceToOutputPdu, + PixelFormat, Point, QuantQuality, QueueDepth, ResetGraphicsPdu, SolidFillPdu, StartFramePdu, SurfaceToCachePdu, + SurfaceToSurfacePdu, Timestamp, WireToSurface1Pdu, WireToSurface2Pdu, +}; + +pub const WIRE_TO_SURFACE_1_BUFFER: [u8; 218] = [ + 0x00, 0x00, 0x08, 0x00, 0x20, 0xa5, 0x03, 0xde, 0x02, 0xab, 0x03, 0xe7, 0x02, 0xc9, 0x00, 0x00, 0x00, 0x01, 0x0e, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x3f, 0x27, 0x19, 0x82, 0x72, 0x69, 0x40, 0x28, 0x1a, 0x3f, 0x27, + 0x19, 0x40, 0x28, 0x1a, 0x41, 0x29, 0x1b, 0x4f, 0x39, 0x2c, 0xa0, 0x94, 0x8d, 0xc0, 0xb8, 0xb3, 0x00, 0x09, 0xd8, + 0xd3, 0xd0, 0x97, 0x8a, 0x82, 0x40, 0x28, 0x1a, 0x41, 0x29, 0x1b, 0x3f, 0x27, 0x19, 0x4f, 0x39, 0x2c, 0xdf, 0xdb, + 0xd9, 0xd8, 0xd3, 0xd0, 0xff, 0xff, 0xff, 0x00, 0x09, 0xff, 0xff, 0xff, 0x3f, 0x27, 0x19, 0x41, 0x29, 0x1b, 0x40, + 0x28, 0x1a, 0x40, 0x28, 0x1a, 0xe5, 0xe1, 0xe0, 0x81, 0x71, 0x68, 0x40, 0x28, 0x1a, 0xff, 0xff, 0xff, 0x00, 0x09, + 0xff, 0xff, 0xff, 0x60, 0x4b, 0x40, 0x4f, 0x39, 0x2c, 0x60, 0x4b, 0x40, 0xd8, 0xd3, 0xd0, 0xc0, 0xb8, 0xb3, 0x43, + 0x2b, 0x1d, 0x3f, 0x27, 0x19, 0xff, 0xff, 0xff, 0x00, 0x09, 0xc0, 0xb8, 0xb3, 0xef, 0xed, 0xeb, 0xdf, 0xdb, 0xd9, + 0xea, 0xe7, 0xe6, 0xc0, 0xb8, 0xb3, 0x41, 0x29, 0x1b, 0x41, 0x29, 0x1b, 0x42, 0x2a, 0x1c, 0xff, 0xff, 0xff, 0x00, + 0x09, 0x41, 0x29, 0x1b, 0x81, 0x71, 0x68, 0x80, 0x71, 0x67, 0x5f, 0x4b, 0x3f, 0x40, 0x28, 0x1a, 0x42, 0x2a, 0x1c, + 0x40, 0x28, 0x1a, 0x3f, 0x27, 0x19, 0xc0, 0xb8, 0xb3, +]; + +pub const WIRE_TO_SURFACE_2_BUFFER: [u8; 629] = [ + 0x00, 0x00, 0x09, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0x68, 0x02, 0x00, 0x00, 0xc1, 0xcc, 0x0c, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0xc4, 0xcc, 0x56, 0x02, 0x00, 0x00, 0x40, 0x01, 0x00, 0x01, 0x00, 0x01, 0x02, + 0x00, 0x37, 0x02, 0x00, 0x00, 0x63, 0x02, 0x51, 0x01, 0x45, 0x00, 0x29, 0x00, 0x66, 0x76, 0x88, 0x99, 0xa9, 0xc6, + 0xcc, 0x53, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x05, 0x00, 0x01, 0xff, 0x9a, 0x00, 0x4a, 0x00, 0x58, + 0x00, 0x00, 0x00, 0x00, 0x09, 0x91, 0x02, 0xc6, 0xd9, 0x08, 0x8c, 0x99, 0x11, 0xb3, 0xc9, 0x0f, 0x88, 0x8d, 0x99, + 0x1b, 0x32, 0x36, 0x64, 0x6c, 0xc8, 0xd9, 0x91, 0xb3, 0x23, 0x66, 0x46, 0xcc, 0x8d, 0x99, 0x1b, 0x32, 0x36, 0x64, + 0x6c, 0xc8, 0xd9, 0x91, 0xb3, 0x23, 0x66, 0x46, 0xc0, 0x0b, 0x3a, 0x05, 0x02, 0x82, 0x85, 0x14, 0x52, 0xaa, 0xaf, + 0xa0, 0x00, 0x06, 0x4e, 0x0d, 0x4e, 0x87, 0xe8, 0xfa, 0x38, 0x81, 0x5c, 0x42, 0xf1, 0x11, 0x44, 0x8f, 0x8f, 0xc5, + 0xe2, 0xf0, 0x32, 0x20, 0x66, 0x8a, 0x55, 0x55, 0x00, 0x03, 0x19, 0x29, 0x00, 0x05, 0xee, 0x85, 0x49, 0xe8, 0xaa, + 0x77, 0xf5, 0x97, 0x8b, 0xda, 0x20, 0x80, 0x52, 0x8a, 0x27, 0x0f, 0x77, 0x70, 0x01, 0x4b, 0x54, 0x53, 0x4e, 0x00, + 0x78, 0x64, 0x8a, 0x16, 0x27, 0xe8, 0x5f, 0xbf, 0xff, 0xff, 0x00, 0x05, 0x1e, 0x50, 0xf7, 0xfe, 0x80, 0x02, 0x87, + 0x64, 0x1c, 0xf6, 0x00, 0x0a, 0x3b, 0x43, 0x9f, 0xe8, 0x00, 0x29, 0xe4, 0x86, 0xff, 0x80, 0x00, 0xa3, 0x68, 0x6f, + 0xf0, 0x00, 0x32, 0x23, 0x00, 0x00, 0x09, 0xa1, 0x78, 0xf8, 0x5c, 0x2e, 0x1e, 0x1e, 0x0b, 0x0f, 0x05, 0x87, 0x82, + 0xc3, 0xc1, 0x61, 0xe0, 0xb0, 0x01, 0x5e, 0x01, 0xf3, 0x53, 0x01, 0x0f, 0x23, 0xc0, 0x93, 0x90, 0x4a, 0x06, 0x80, + 0x49, 0x4f, 0x42, 0x91, 0x16, 0x1a, 0x40, 0x14, 0x41, 0x0a, 0x00, 0xa9, 0x24, 0x40, 0x4f, 0x78, 0x9a, 0x28, 0x35, + 0x49, 0xef, 0x83, 0xc4, 0xfb, 0xf8, 0x0f, 0x13, 0xdf, 0x03, 0xda, 0xff, 0xf5, 0x80, 0x74, 0x27, 0xb4, 0x0e, 0x5f, + 0xea, 0x0c, 0x90, 0x00, 0x0a, 0xc1, 0x78, 0xf8, 0x5c, 0x2e, 0x1e, 0x1e, 0x0b, 0x0f, 0x05, 0x87, 0x82, 0xc3, 0xc1, + 0x61, 0xe0, 0xb0, 0xf0, 0x58, 0x3d, 0x00, 0x61, 0x90, 0x04, 0x02, 0x02, 0x04, 0x08, 0x21, 0x90, 0xd8, 0x00, 0x00, + 0x3f, 0x62, 0x0a, 0x3a, 0x04, 0x11, 0x08, 0xd0, 0x89, 0x15, 0xd4, 0x8c, 0x4b, 0x4f, 0x09, 0xa5, 0x2a, 0x10, 0x8d, + 0x80, 0x1a, 0xd1, 0xa5, 0x60, 0x0c, 0xe8, 0x11, 0x41, 0x1b, 0x2b, 0xe1, 0x1f, 0xfe, 0x43, 0xff, 0x81, 0x8f, 0x84, + 0x3d, 0x08, 0x1f, 0xf8, 0x87, 0xa0, 0x9a, 0x85, 0xf0, 0x27, 0x42, 0x7f, 0x01, 0xe6, 0x80, 0xc6, 0xcc, 0xe4, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x05, 0x00, 0x01, 0xff, 0x82, 0x00, 0x1d, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x00, 0x0a, 0xe8, 0x1c, 0xc8, 0x87, 0xe8, 0x0d, 0x01, 0xa0, 0x68, 0x34, 0x34, 0x34, 0x69, 0xad, 0x6e, 0xeb, 0x6d, + 0xb6, 0x80, 0x00, 0x0c, 0x64, 0x1b, 0x0d, 0x0f, 0xa3, 0xe8, 0xfa, 0x7d, 0x7d, 0xfb, 0xf3, 0xe7, 0xc5, 0xc0, 0xa4, + 0x81, 0x02, 0x48, 0x44, 0x6c, 0x09, 0xad, 0xd7, 0x55, 0x70, 0x00, 0x18, 0x48, 0x08, 0x10, 0x41, 0x04, 0x20, 0x00, + 0x15, 0xfd, 0x00, 0x9b, 0xd0, 0x2d, 0xe8, 0x38, 0x9d, 0x02, 0xf4, 0x0b, 0xc7, 0x05, 0x34, 0x69, 0xbf, 0x78, 0x80, + 0xb2, 0xed, 0xd4, 0x00, 0x61, 0xd0, 0xe8, 0xb2, 0xcb, 0x00, 0x01, 0x01, 0xc2, 0x0b, 0x42, 0xa2, 0xff, 0xe2, 0x17, + 0xff, 0xec, 0x01, 0xfe, 0x48, 0x04, 0xff, 0x14, 0x10, 0xfe, 0x40, 0x09, 0x7f, 0x12, 0x00, 0xe6, 0x20, 0x0f, 0xd4, + 0x80, 0x7e, 0xc0, 0x39, 0xfa, 0x90, 0x0f, 0xb0, 0x0e, 0x3d, 0x48, 0x46, 0x88, 0x0b, 0xa4, 0x00, 0x00, 0x00, 0x1d, + 0x66, 0x40, 0x34, 0x02, 0x87, 0xa0, 0x25, 0x9a, 0x01, 0x21, 0x0a, 0x48, 0x91, 0x49, 0x44, 0x28, 0x88, 0x43, 0xbc, + 0x84, 0x2c, 0x42, 0x38, 0x56, 0x0b, 0x00, 0x00, 0x00, 0x1b, 0xfc, 0x40, 0xe4, 0x1c, 0x87, 0x21, 0xca, 0x95, 0x04, + 0x08, 0x10, 0x42, 0x18, 0xc0, 0x09, 0x68, 0xa5, 0x54, 0x00, 0x61, 0x40, 0xe6, 0x97, 0xcd, 0xee, 0xcf, 0xb8, 0x3f, + 0xf1, 0x23, 0xa8, 0x7f, 0x86, 0x1a, 0x8f, 0xf0, 0x2c, 0x49, 0x88, 0xbe, 0x16, 0xc5, 0xe0, 0xc2, 0xcc, 0x06, 0x00, + 0x00, 0x00, +]; + +pub const DELETE_ENCODING_CONTEXT_BUFFER: [u8; 6] = [0x00, 0x00, 0x01, 0x00, 0x00, 0x00]; + +pub const SOLID_FILL_BUFFER: [u8; 16] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x40, 0x00, +]; + +pub const SURFACE_TO_SURFACE_BUFFER: [u8; 18] = [ + 0x00, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x3c, 0x00, 0xa4, 0x02, 0x94, 0x00, 0x01, 0x00, 0x80, 0x00, 0x3c, 0x00, +]; + +pub const SURFACE_TO_CACHE_BUFFER: [u8; 20] = [ + 0x00, 0x00, 0xb7, 0x7f, 0xa3, 0xa6, 0xda, 0x86, 0x3d, 0x11, 0x0e, 0x00, 0x80, 0x02, 0x00, 0x00, 0xc0, 0x02, 0x40, + 0x00, +]; + +pub const CACHE_TO_SURFACE_BUFFER: [u8; 10] = [0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x40, 0x01]; + +pub const CREATE_SURFACE_BUFFER: [u8; 7] = [0x00, 0x00, 0x00, 0x04, 0x00, 0x03, 0x21]; + +pub const DELETE_SURFACE_BUFFER: [u8; 2] = [0x00, 0x00]; + +pub const RESET_GRAPHICS_BUFFER: [u8; 332] = [ + 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x03, 0x00, 0x00, 0xff, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub const MAP_SURFACE_TO_OUTPUT_BUFFER: [u8; 12] = + [0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2, 0x00, 0x00, 0x00]; + +pub const EVICT_CACHE_ENTRY_BUFFER: [u8; 2] = [0x00, 0x00]; + +pub const START_FRAME_BUFFER: [u8; 8] = [0xf7, 0xe8, 0x9b, 0x5, 0x05, 0x00, 0x00, 0x00]; + +pub const END_FRAME_BUFFER: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; + +pub const CAPABILITIES_CONFIRM_BUFFER: [u8; 12] = + [0x02, 0x05, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00]; + +pub const CAPABILITIES_ADVERTISE_BUFFER: [u8; 122] = [ + 0x9, 0x0, 0x4, 0x0, 0x8, 0x0, 0x4, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5, 0x1, 0x8, 0x0, 0x4, 0x0, 0x0, 0x0, 0x1, + 0x0, 0x0, 0x0, 0x2, 0x0, 0xa, 0x0, 0x4, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0, 0x1, 0xa, 0x0, 0x10, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xa, 0x0, 0x4, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x1, 0x3, 0xa, 0x0, 0x4, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0, 0x4, 0xa, 0x0, + 0x4, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x2, 0x5, 0xa, 0x0, 0x4, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0, 0x6, + 0xa, 0x0, 0x4, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, +]; + +pub const FRAME_ACKNOWLEDGE_BUFFER: [u8; 12] = [0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0]; + +pub const CACHE_IMPORT_REPLY_BUFFER: [u8; 1840] = [ + 0x97, 0x3, 0x2, 0x0, 0x3, 0x0, 0x4, 0x0, 0x5, 0x0, 0x6, 0x0, 0x7, 0x0, 0x8, 0x0, 0x9, 0x0, 0xa, 0x0, 0xb, 0x0, 0xc, + 0x0, 0xd, 0x0, 0xe, 0x0, 0xf, 0x0, 0x10, 0x0, 0x11, 0x0, 0x12, 0x0, 0x13, 0x0, 0x14, 0x0, 0x15, 0x0, 0x16, 0x0, + 0x17, 0x0, 0x18, 0x0, 0x19, 0x0, 0x1a, 0x0, 0x1b, 0x0, 0x1c, 0x0, 0x1d, 0x0, 0x1e, 0x0, 0x1f, 0x0, 0x20, 0x0, 0x21, + 0x0, 0x22, 0x0, 0x23, 0x0, 0x24, 0x0, 0x25, 0x0, 0x26, 0x0, 0x27, 0x0, 0x28, 0x0, 0x29, 0x0, 0x2a, 0x0, 0x2b, 0x0, + 0x2c, 0x0, 0x2d, 0x0, 0x2e, 0x0, 0x2f, 0x0, 0x30, 0x0, 0x31, 0x0, 0x32, 0x0, 0x33, 0x0, 0x34, 0x0, 0x35, 0x0, 0x36, + 0x0, 0x37, 0x0, 0x38, 0x0, 0x39, 0x0, 0x3a, 0x0, 0x3b, 0x0, 0x3c, 0x0, 0x3d, 0x0, 0x3e, 0x0, 0x3f, 0x0, 0x40, 0x0, + 0x41, 0x0, 0x42, 0x0, 0x43, 0x0, 0x44, 0x0, 0x45, 0x0, 0x46, 0x0, 0x47, 0x0, 0x48, 0x0, 0x49, 0x0, 0x4a, 0x0, 0x4b, + 0x0, 0x4c, 0x0, 0x4d, 0x0, 0x4e, 0x0, 0x4f, 0x0, 0x50, 0x0, 0x51, 0x0, 0x52, 0x0, 0x53, 0x0, 0x54, 0x0, 0x55, 0x0, + 0x56, 0x0, 0x57, 0x0, 0x58, 0x0, 0x59, 0x0, 0x5a, 0x0, 0x5b, 0x0, 0x5c, 0x0, 0x5d, 0x0, 0x5e, 0x0, 0x5f, 0x0, 0x60, + 0x0, 0x61, 0x0, 0x62, 0x0, 0x63, 0x0, 0x64, 0x0, 0x65, 0x0, 0x66, 0x0, 0x67, 0x0, 0x68, 0x0, 0x69, 0x0, 0x6a, 0x0, + 0x6b, 0x0, 0x6c, 0x0, 0x6d, 0x0, 0x6e, 0x0, 0x6f, 0x0, 0x70, 0x0, 0x71, 0x0, 0x72, 0x0, 0x73, 0x0, 0x74, 0x0, 0x75, + 0x0, 0x76, 0x0, 0x77, 0x0, 0x78, 0x0, 0x79, 0x0, 0x7a, 0x0, 0x7b, 0x0, 0x7c, 0x0, 0x7d, 0x0, 0x7e, 0x0, 0x7f, 0x0, + 0x80, 0x0, 0x81, 0x0, 0x82, 0x0, 0x83, 0x0, 0x84, 0x0, 0x85, 0x0, 0x86, 0x0, 0x87, 0x0, 0x88, 0x0, 0x89, 0x0, 0x8a, + 0x0, 0x8b, 0x0, 0x8c, 0x0, 0x8d, 0x0, 0x8e, 0x0, 0x8f, 0x0, 0x90, 0x0, 0x91, 0x0, 0x92, 0x0, 0x93, 0x0, 0x94, 0x0, + 0x95, 0x0, 0x96, 0x0, 0x97, 0x0, 0x98, 0x0, 0x99, 0x0, 0x9a, 0x0, 0x9b, 0x0, 0x9c, 0x0, 0x9d, 0x0, 0x9e, 0x0, 0x9f, + 0x0, 0xa0, 0x0, 0xa1, 0x0, 0xa2, 0x0, 0xa3, 0x0, 0xa4, 0x0, 0xa5, 0x0, 0xa6, 0x0, 0xa7, 0x0, 0xa8, 0x0, 0xa9, 0x0, + 0xaa, 0x0, 0xab, 0x0, 0xac, 0x0, 0xad, 0x0, 0xae, 0x0, 0xaf, 0x0, 0xb0, 0x0, 0xb1, 0x0, 0xb2, 0x0, 0xb3, 0x0, 0xb4, + 0x0, 0xb5, 0x0, 0xb6, 0x0, 0xb7, 0x0, 0xb8, 0x0, 0xb9, 0x0, 0xba, 0x0, 0xbb, 0x0, 0xbc, 0x0, 0xbd, 0x0, 0xbe, 0x0, + 0xbf, 0x0, 0xc0, 0x0, 0xc1, 0x0, 0xc2, 0x0, 0xc3, 0x0, 0xc4, 0x0, 0xc5, 0x0, 0xc6, 0x0, 0xc7, 0x0, 0xc8, 0x0, 0xc9, + 0x0, 0xca, 0x0, 0xcb, 0x0, 0xcc, 0x0, 0xcd, 0x0, 0xce, 0x0, 0xcf, 0x0, 0xd0, 0x0, 0xd1, 0x0, 0xd2, 0x0, 0xd3, 0x0, + 0xd4, 0x0, 0xd5, 0x0, 0xd6, 0x0, 0xd7, 0x0, 0xd8, 0x0, 0xd9, 0x0, 0xda, 0x0, 0xdb, 0x0, 0xdc, 0x0, 0xdd, 0x0, 0xde, + 0x0, 0xdf, 0x0, 0xe0, 0x0, 0xe1, 0x0, 0xe2, 0x0, 0xe3, 0x0, 0xe4, 0x0, 0xe5, 0x0, 0xe6, 0x0, 0xe7, 0x0, 0xe8, 0x0, + 0xe9, 0x0, 0xea, 0x0, 0xeb, 0x0, 0xec, 0x0, 0xed, 0x0, 0xee, 0x0, 0xef, 0x0, 0xf0, 0x0, 0xf1, 0x0, 0xf2, 0x0, 0xf3, + 0x0, 0xf4, 0x0, 0xf5, 0x0, 0xf6, 0x0, 0xf7, 0x0, 0xf8, 0x0, 0xf9, 0x0, 0xfa, 0x0, 0xfb, 0x0, 0xfc, 0x0, 0xfd, 0x0, + 0xfe, 0x0, 0xff, 0x0, 0x0, 0x1, 0x1, 0x1, 0x2, 0x1, 0x3, 0x1, 0x4, 0x1, 0x5, 0x1, 0x6, 0x1, 0x7, 0x1, 0x8, 0x1, + 0x9, 0x1, 0xa, 0x1, 0xb, 0x1, 0xc, 0x1, 0xd, 0x1, 0xe, 0x1, 0xf, 0x1, 0x10, 0x1, 0x11, 0x1, 0x12, 0x1, 0x13, 0x1, + 0x14, 0x1, 0x15, 0x1, 0x16, 0x1, 0x17, 0x1, 0x18, 0x1, 0x19, 0x1, 0x1a, 0x1, 0x1b, 0x1, 0x1c, 0x1, 0x1d, 0x1, 0x1e, + 0x1, 0x1f, 0x1, 0x20, 0x1, 0x21, 0x1, 0x22, 0x1, 0x23, 0x1, 0x24, 0x1, 0x25, 0x1, 0x26, 0x1, 0x27, 0x1, 0x28, 0x1, + 0x29, 0x1, 0x2a, 0x1, 0x2b, 0x1, 0x2c, 0x1, 0x2d, 0x1, 0x2e, 0x1, 0x2f, 0x1, 0x30, 0x1, 0x31, 0x1, 0x32, 0x1, 0x33, + 0x1, 0x34, 0x1, 0x35, 0x1, 0x36, 0x1, 0x37, 0x1, 0x38, 0x1, 0x39, 0x1, 0x3a, 0x1, 0x3b, 0x1, 0x3c, 0x1, 0x3d, 0x1, + 0x3e, 0x1, 0x3f, 0x1, 0x40, 0x1, 0x41, 0x1, 0x42, 0x1, 0x43, 0x1, 0x44, 0x1, 0x45, 0x1, 0x46, 0x1, 0x47, 0x1, 0x48, + 0x1, 0x49, 0x1, 0x4a, 0x1, 0x4b, 0x1, 0x4c, 0x1, 0x4d, 0x1, 0x4e, 0x1, 0x4f, 0x1, 0x50, 0x1, 0x51, 0x1, 0x52, 0x1, + 0x53, 0x1, 0x54, 0x1, 0x55, 0x1, 0x56, 0x1, 0x57, 0x1, 0x58, 0x1, 0x59, 0x1, 0x5a, 0x1, 0x5b, 0x1, 0x5c, 0x1, 0x5d, + 0x1, 0x5e, 0x1, 0x5f, 0x1, 0x60, 0x1, 0x61, 0x1, 0x62, 0x1, 0x63, 0x1, 0x64, 0x1, 0x65, 0x1, 0x66, 0x1, 0x67, 0x1, + 0x68, 0x1, 0x69, 0x1, 0x6a, 0x1, 0x6b, 0x1, 0x6c, 0x1, 0x6d, 0x1, 0x6e, 0x1, 0x6f, 0x1, 0x70, 0x1, 0x71, 0x1, 0x72, + 0x1, 0x73, 0x1, 0x74, 0x1, 0x75, 0x1, 0x76, 0x1, 0x77, 0x1, 0x78, 0x1, 0x79, 0x1, 0x7a, 0x1, 0x7b, 0x1, 0x7c, 0x1, + 0x7d, 0x1, 0x7e, 0x1, 0x7f, 0x1, 0x80, 0x1, 0x81, 0x1, 0x82, 0x1, 0x83, 0x1, 0x84, 0x1, 0x85, 0x1, 0x86, 0x1, 0x87, + 0x1, 0x88, 0x1, 0x89, 0x1, 0x8a, 0x1, 0x8b, 0x1, 0x8c, 0x1, 0x8d, 0x1, 0x8e, 0x1, 0x8f, 0x1, 0x90, 0x1, 0x91, 0x1, + 0x92, 0x1, 0x93, 0x1, 0x94, 0x1, 0x95, 0x1, 0x96, 0x1, 0x97, 0x1, 0x98, 0x1, 0x99, 0x1, 0x9a, 0x1, 0x9b, 0x1, 0x9c, + 0x1, 0x9d, 0x1, 0x9e, 0x1, 0x9f, 0x1, 0xa0, 0x1, 0xa1, 0x1, 0xa2, 0x1, 0xa3, 0x1, 0xa4, 0x1, 0xa5, 0x1, 0xa6, 0x1, + 0xa7, 0x1, 0xa8, 0x1, 0xa9, 0x1, 0xaa, 0x1, 0xab, 0x1, 0xac, 0x1, 0xad, 0x1, 0xae, 0x1, 0xaf, 0x1, 0xb0, 0x1, 0xb1, + 0x1, 0xb2, 0x1, 0xb3, 0x1, 0xb4, 0x1, 0xb5, 0x1, 0xb6, 0x1, 0xb7, 0x1, 0xb8, 0x1, 0xb9, 0x1, 0xba, 0x1, 0xbb, 0x1, + 0xbc, 0x1, 0xbd, 0x1, 0xbe, 0x1, 0xbf, 0x1, 0xc0, 0x1, 0xc1, 0x1, 0xc2, 0x1, 0xc3, 0x1, 0xc4, 0x1, 0xc5, 0x1, 0xc6, + 0x1, 0xc7, 0x1, 0xc8, 0x1, 0xc9, 0x1, 0xca, 0x1, 0xcb, 0x1, 0xcc, 0x1, 0xcd, 0x1, 0xce, 0x1, 0xcf, 0x1, 0xd0, 0x1, + 0xd1, 0x1, 0xd2, 0x1, 0xd3, 0x1, 0xd4, 0x1, 0xd5, 0x1, 0xd6, 0x1, 0xd7, 0x1, 0xd8, 0x1, 0xd9, 0x1, 0xda, 0x1, 0xdb, + 0x1, 0xdc, 0x1, 0xdd, 0x1, 0xde, 0x1, 0xdf, 0x1, 0xe0, 0x1, 0xe1, 0x1, 0xe2, 0x1, 0xe3, 0x1, 0xe4, 0x1, 0xe5, 0x1, + 0xe6, 0x1, 0xe7, 0x1, 0xe8, 0x1, 0xe9, 0x1, 0xea, 0x1, 0xeb, 0x1, 0xec, 0x1, 0xed, 0x1, 0xee, 0x1, 0xef, 0x1, 0xf0, + 0x1, 0xf1, 0x1, 0xf2, 0x1, 0xf3, 0x1, 0xf4, 0x1, 0xf5, 0x1, 0xf6, 0x1, 0xf7, 0x1, 0xf8, 0x1, 0xf9, 0x1, 0xfa, 0x1, + 0xfb, 0x1, 0xfc, 0x1, 0xfd, 0x1, 0xfe, 0x1, 0xff, 0x1, 0x0, 0x2, 0x1, 0x2, 0x2, 0x2, 0x3, 0x2, 0x4, 0x2, 0x5, 0x2, + 0x6, 0x2, 0x7, 0x2, 0x8, 0x2, 0x9, 0x2, 0xa, 0x2, 0xb, 0x2, 0xc, 0x2, 0xd, 0x2, 0xe, 0x2, 0xf, 0x2, 0x10, 0x2, + 0x11, 0x2, 0x12, 0x2, 0x13, 0x2, 0x14, 0x2, 0x15, 0x2, 0x16, 0x2, 0x17, 0x2, 0x18, 0x2, 0x19, 0x2, 0x1a, 0x2, 0x1b, + 0x2, 0x1c, 0x2, 0x1d, 0x2, 0x1e, 0x2, 0x1f, 0x2, 0x20, 0x2, 0x21, 0x2, 0x22, 0x2, 0x23, 0x2, 0x24, 0x2, 0x25, 0x2, + 0x26, 0x2, 0x27, 0x2, 0x28, 0x2, 0x29, 0x2, 0x2a, 0x2, 0x2b, 0x2, 0x2c, 0x2, 0x2d, 0x2, 0x2e, 0x2, 0x2f, 0x2, 0x30, + 0x2, 0x31, 0x2, 0x32, 0x2, 0x33, 0x2, 0x34, 0x2, 0x35, 0x2, 0x36, 0x2, 0x37, 0x2, 0x38, 0x2, 0x39, 0x2, 0x3a, 0x2, + 0x3b, 0x2, 0x3c, 0x2, 0x3d, 0x2, 0x3e, 0x2, 0x3f, 0x2, 0x40, 0x2, 0x41, 0x2, 0x42, 0x2, 0x43, 0x2, 0x44, 0x2, 0x45, + 0x2, 0x46, 0x2, 0x47, 0x2, 0x48, 0x2, 0x49, 0x2, 0x4a, 0x2, 0x4b, 0x2, 0x4c, 0x2, 0x4d, 0x2, 0x4e, 0x2, 0x4f, 0x2, + 0x50, 0x2, 0x51, 0x2, 0x52, 0x2, 0x53, 0x2, 0x54, 0x2, 0x55, 0x2, 0x56, 0x2, 0x57, 0x2, 0x58, 0x2, 0x59, 0x2, 0x5a, + 0x2, 0x5b, 0x2, 0x5c, 0x2, 0x5d, 0x2, 0x5e, 0x2, 0x5f, 0x2, 0x60, 0x2, 0x61, 0x2, 0x62, 0x2, 0x63, 0x2, 0x64, 0x2, + 0x65, 0x2, 0x66, 0x2, 0x67, 0x2, 0x68, 0x2, 0x69, 0x2, 0x6a, 0x2, 0x6b, 0x2, 0x6c, 0x2, 0x6d, 0x2, 0x6e, 0x2, 0x6f, + 0x2, 0x70, 0x2, 0x71, 0x2, 0x72, 0x2, 0x73, 0x2, 0x74, 0x2, 0x75, 0x2, 0x76, 0x2, 0x77, 0x2, 0x78, 0x2, 0x79, 0x2, + 0x7a, 0x2, 0x7b, 0x2, 0x7c, 0x2, 0x7d, 0x2, 0x7e, 0x2, 0x7f, 0x2, 0x80, 0x2, 0x81, 0x2, 0x82, 0x2, 0x83, 0x2, 0x84, + 0x2, 0x85, 0x2, 0x86, 0x2, 0x87, 0x2, 0x88, 0x2, 0x89, 0x2, 0x8a, 0x2, 0x8b, 0x2, 0x8c, 0x2, 0x8d, 0x2, 0x8e, 0x2, + 0x8f, 0x2, 0x90, 0x2, 0x91, 0x2, 0x92, 0x2, 0x93, 0x2, 0x94, 0x2, 0x95, 0x2, 0x96, 0x2, 0x97, 0x2, 0x98, 0x2, 0x99, + 0x2, 0x9a, 0x2, 0x9b, 0x2, 0x9c, 0x2, 0x9d, 0x2, 0x9e, 0x2, 0x9f, 0x2, 0xa0, 0x2, 0xa1, 0x2, 0xa2, 0x2, 0xa3, 0x2, + 0xa4, 0x2, 0xa5, 0x2, 0xa6, 0x2, 0xa7, 0x2, 0xa8, 0x2, 0xa9, 0x2, 0xaa, 0x2, 0xab, 0x2, 0xac, 0x2, 0xad, 0x2, 0xae, + 0x2, 0xaf, 0x2, 0xb0, 0x2, 0xb1, 0x2, 0xb2, 0x2, 0xb3, 0x2, 0xb4, 0x2, 0xb5, 0x2, 0xb6, 0x2, 0xb7, 0x2, 0xb8, 0x2, + 0xb9, 0x2, 0xba, 0x2, 0xbb, 0x2, 0xbc, 0x2, 0xbd, 0x2, 0xbe, 0x2, 0xbf, 0x2, 0xc0, 0x2, 0xc1, 0x2, 0xc2, 0x2, 0xc3, + 0x2, 0xc4, 0x2, 0xc5, 0x2, 0xc6, 0x2, 0xc7, 0x2, 0xc8, 0x2, 0xc9, 0x2, 0xca, 0x2, 0xcb, 0x2, 0xcc, 0x2, 0xcd, 0x2, + 0xce, 0x2, 0xcf, 0x2, 0xd0, 0x2, 0xd1, 0x2, 0xd2, 0x2, 0xd3, 0x2, 0xd4, 0x2, 0xd5, 0x2, 0xd6, 0x2, 0xd7, 0x2, 0xd8, + 0x2, 0xd9, 0x2, 0xda, 0x2, 0xdb, 0x2, 0xdc, 0x2, 0xdd, 0x2, 0xde, 0x2, 0xdf, 0x2, 0xe0, 0x2, 0xe1, 0x2, 0xe2, 0x2, + 0xe3, 0x2, 0xe4, 0x2, 0xe5, 0x2, 0xe6, 0x2, 0xe7, 0x2, 0xe8, 0x2, 0xe9, 0x2, 0xea, 0x2, 0xeb, 0x2, 0xec, 0x2, 0xed, + 0x2, 0xee, 0x2, 0xef, 0x2, 0xf0, 0x2, 0xf1, 0x2, 0xf2, 0x2, 0xf3, 0x2, 0xf4, 0x2, 0xf5, 0x2, 0xf6, 0x2, 0xf7, 0x2, + 0xf8, 0x2, 0xf9, 0x2, 0xfa, 0x2, 0xfb, 0x2, 0xfc, 0x2, 0xfd, 0x2, 0xfe, 0x2, 0xff, 0x2, 0x0, 0x3, 0x1, 0x3, 0x2, + 0x3, 0x3, 0x3, 0x4, 0x3, 0x5, 0x3, 0x6, 0x3, 0x7, 0x3, 0x8, 0x3, 0x9, 0x3, 0xa, 0x3, 0xb, 0x3, 0xc, 0x3, 0xd, 0x3, + 0xe, 0x3, 0xf, 0x3, 0x10, 0x3, 0x11, 0x3, 0x12, 0x3, 0x13, 0x3, 0x14, 0x3, 0x15, 0x3, 0x16, 0x3, 0x17, 0x3, 0x18, + 0x3, 0x19, 0x3, 0x1a, 0x3, 0x1b, 0x3, 0x1c, 0x3, 0x1d, 0x3, 0x1e, 0x3, 0x1f, 0x3, 0x20, 0x3, 0x21, 0x3, 0x22, 0x3, + 0x23, 0x3, 0x24, 0x3, 0x25, 0x3, 0x26, 0x3, 0x27, 0x3, 0x28, 0x3, 0x29, 0x3, 0x2a, 0x3, 0x2b, 0x3, 0x2c, 0x3, 0x2d, + 0x3, 0x2e, 0x3, 0x2f, 0x3, 0x30, 0x3, 0x31, 0x3, 0x32, 0x3, 0x33, 0x3, 0x34, 0x3, 0x35, 0x3, 0x36, 0x3, 0x37, 0x3, + 0x38, 0x3, 0x39, 0x3, 0x3a, 0x3, 0x3b, 0x3, 0x3c, 0x3, 0x3d, 0x3, 0x3e, 0x3, 0x3f, 0x3, 0x40, 0x3, 0x41, 0x3, 0x42, + 0x3, 0x43, 0x3, 0x44, 0x3, 0x45, 0x3, 0x46, 0x3, 0x47, 0x3, 0x48, 0x3, 0x49, 0x3, 0x4a, 0x3, 0x4b, 0x3, 0x4c, 0x3, + 0x4d, 0x3, 0x4e, 0x3, 0x4f, 0x3, 0x50, 0x3, 0x51, 0x3, 0x52, 0x3, 0x53, 0x3, 0x54, 0x3, 0x55, 0x3, 0x56, 0x3, 0x57, + 0x3, 0x58, 0x3, 0x59, 0x3, 0x5a, 0x3, 0x5b, 0x3, 0x5c, 0x3, 0x5d, 0x3, 0x5e, 0x3, 0x5f, 0x3, 0x60, 0x3, 0x61, 0x3, + 0x62, 0x3, 0x63, 0x3, 0x64, 0x3, 0x65, 0x3, 0x66, 0x3, 0x67, 0x3, 0x68, 0x3, 0x69, 0x3, 0x6a, 0x3, 0x6b, 0x3, 0x6c, + 0x3, 0x6d, 0x3, 0x6e, 0x3, 0x6f, 0x3, 0x70, 0x3, 0x71, 0x3, 0x72, 0x3, 0x73, 0x3, 0x74, 0x3, 0x75, 0x3, 0x76, 0x3, + 0x77, 0x3, 0x78, 0x3, 0x79, 0x3, 0x7a, 0x3, 0x7b, 0x3, 0x7c, 0x3, 0x7d, 0x3, 0x7e, 0x3, 0x7f, 0x3, 0x80, 0x3, 0x81, + 0x3, 0x82, 0x3, 0x83, 0x3, 0x84, 0x3, 0x85, 0x3, 0x86, 0x3, 0x87, 0x3, 0x88, 0x3, 0x89, 0x3, 0x8a, 0x3, 0x8b, 0x3, + 0x8c, 0x3, 0x8d, 0x3, 0x8e, 0x3, 0x8f, 0x3, 0x90, 0x3, 0x91, 0x3, 0x92, 0x3, 0x93, 0x3, 0x94, 0x3, 0x95, 0x3, 0x96, + 0x3, 0x97, 0x3, 0x98, 0x3, +]; + +pub const AVC_444_MESSAGE_INCORRECT_LEN: [u8; 88] = [ + 0x0, 0x0, 0x0, 0x80, 0x1, 0x0, 0x0, 0x0, 0x0, 0x7, 0x20, 0x4, 0x10, 0x7, 0x30, 0x4, 0x16, 0x64, 0x0, 0x0, 0x0, 0x1, + 0x61, 0x9a, 0x11, 0xda, 0x24, 0xea, 0x25, 0x0, 0x1f, 0xe6, 0x0, 0x0, 0x0, 0x1, 0x61, 0x0, 0x3f, 0xc9, 0xa1, 0x1d, + 0xa2, 0x4e, 0xa2, 0x50, 0x1, 0xfe, 0x60, 0x0, 0x0, 0x0, 0x1, 0x61, 0x0, 0x1f, 0xe2, 0x68, 0x47, 0x68, 0x93, 0xa8, + 0x94, 0x0, 0x7f, 0x98, 0x0, 0x0, 0x0, 0x1, 0x61, 0x0, 0xb, 0xf4, 0x9a, 0x11, 0xda, 0x24, 0xea, 0x25, 0x0, 0x1d, + 0xe7, 0x97, 0xab, 0x80, 0x80, 0x80, +]; + +pub const AVC_444_MESSAGE_CORRECT_LEN: [u8; 88] = [ + 0x54, 0x0, 0x0, 0x80, 0x1, 0x0, 0x0, 0x0, 0x0, 0x7, 0x20, 0x4, 0x10, 0x7, 0x30, 0x4, 0x16, 0x64, 0x0, 0x0, 0x0, + 0x1, 0x61, 0x9a, 0x11, 0xda, 0x24, 0xea, 0x25, 0x0, 0x1f, 0xe6, 0x0, 0x0, 0x0, 0x1, 0x61, 0x0, 0x3f, 0xc9, 0xa1, + 0x1d, 0xa2, 0x4e, 0xa2, 0x50, 0x1, 0xfe, 0x60, 0x0, 0x0, 0x0, 0x1, 0x61, 0x0, 0x1f, 0xe2, 0x68, 0x47, 0x68, 0x93, + 0xa8, 0x94, 0x0, 0x7f, 0x98, 0x0, 0x0, 0x0, 0x1, 0x61, 0x0, 0xb, 0xf4, 0x9a, 0x11, 0xda, 0x24, 0xea, 0x25, 0x0, + 0x1d, 0xe7, 0x97, 0xab, 0x80, 0x80, 0x80, +]; + +pub static WIRE_TO_SURFACE_1: LazyLock = LazyLock::new(|| WireToSurface1Pdu { + surface_id: 0, + codec_id: Codec1Type::ClearCodec, + pixel_format: PixelFormat::XRgb, + destination_rectangle: InclusiveRectangle { + left: 933, + top: 734, + right: 939, + bottom: 743, + }, + bitmap_data: WIRE_TO_SURFACE_1_BUFFER[17..].to_vec(), +}); +pub static WIRE_TO_SURFACE_1_BITMAP_DATA: LazyLock> = LazyLock::new(|| WIRE_TO_SURFACE_1_BUFFER[17..].to_vec()); +pub static WIRE_TO_SURFACE_2: LazyLock = LazyLock::new(|| WireToSurface2Pdu { + surface_id: 0, + codec_id: Codec2Type::RemoteFxProgressive, + codec_context_id: 4, + pixel_format: PixelFormat::XRgb, + bitmap_data: WIRE_TO_SURFACE_2_BUFFER[13..].to_vec(), +}); +pub static WIRE_TO_SURFACE_2_BITMAP_DATA: LazyLock> = LazyLock::new(|| WIRE_TO_SURFACE_2_BUFFER[13..].to_vec()); +pub static DELETE_ENCODING_CONTEXT: LazyLock = LazyLock::new(|| DeleteEncodingContextPdu { + surface_id: 0, + codec_context_id: 1, +}); +pub static SOLID_FILL: LazyLock = LazyLock::new(|| SolidFillPdu { + surface_id: 0, + fill_pixel: Color { + b: 0, + g: 0, + r: 0, + xa: 0, + }, + rectangles: vec![InclusiveRectangle { + left: 0, + top: 0, + right: 64, + bottom: 64, + }], +}); +pub static SURFACE_TO_SURFACE: LazyLock = LazyLock::new(|| SurfaceToSurfacePdu { + source_surface_id: 0, + destination_surface_id: 0, + source_rectangle: InclusiveRectangle { + left: 200, + top: 60, + right: 676, + bottom: 148, + }, + destination_points: vec![Point { x: 128, y: 60 }], +}); +pub static SURFACE_TO_CACHE: LazyLock = LazyLock::new(|| SurfaceToCachePdu { + surface_id: 0, + cache_key: 0x113D_86DA_A6A3_7FB7, + cache_slot: 14, + source_rectangle: InclusiveRectangle { + left: 640, + top: 0, + right: 704, + bottom: 64, + }, +}); +pub static CACHE_TO_SURFACE: LazyLock = LazyLock::new(|| CacheToSurfacePdu { + cache_slot: 2, + surface_id: 0, + destination_points: vec![Point { x: 768, y: 320 }], +}); +pub static CREATE_SURFACE: LazyLock = LazyLock::new(|| CreateSurfacePdu { + surface_id: 0, + width: 1024, + height: 768, + pixel_format: PixelFormat::ARgb, +}); +pub static DELETE_SURFACE: LazyLock = LazyLock::new(|| DeleteSurfacePdu { surface_id: 0 }); +pub static RESET_GRAPHICS: LazyLock = LazyLock::new(|| ResetGraphicsPdu { + width: 1024, + height: 768, + monitors: vec![Monitor { + left: 0, + top: 0, + right: 1023, + bottom: 767, + flags: MonitorFlags::PRIMARY, + }], +}); +pub static MAP_SURFACE_TO_OUTPUT: LazyLock = LazyLock::new(|| MapSurfaceToOutputPdu { + surface_id: 0, + output_origin_x: 1, + output_origin_y: 2, +}); +pub static EVICT_CACHE_ENTRY: LazyLock = LazyLock::new(|| EvictCacheEntryPdu { cache_slot: 0 }); +pub static START_FRAME: LazyLock = LazyLock::new(|| StartFramePdu { + timestamp: Timestamp { + milliseconds: 247, + seconds: 58, + minutes: 27, + hours: 22, + }, + frame_id: 5, +}); +pub static END_FRAME: LazyLock = LazyLock::new(|| EndFramePdu { frame_id: 1 }); +pub static CAPABILITIES_CONFIRM: LazyLock = LazyLock::new(|| { + CapabilitiesConfirmPdu(CapabilitySet::V10_5 { + flags: CapabilitiesV104Flags::AVC_DISABLED, + }) +}); +pub static CAPABILITIES_ADVERTISE: LazyLock = LazyLock::new(|| { + CapabilitiesAdvertisePdu(vec![ + CapabilitySet::V8 { + flags: CapabilitiesV8Flags::THIN_CLIENT, + }, + CapabilitySet::V8_1 { + flags: CapabilitiesV81Flags::THIN_CLIENT, + }, + CapabilitySet::V10 { + flags: CapabilitiesV10Flags::AVC_DISABLED, + }, + CapabilitySet::V10_1, + CapabilitySet::V10_2 { + flags: CapabilitiesV10Flags::AVC_DISABLED, + }, + CapabilitySet::V10_3 { + flags: CapabilitiesV103Flags::AVC_DISABLED, + }, + CapabilitySet::V10_4 { + flags: CapabilitiesV104Flags::AVC_DISABLED, + }, + CapabilitySet::V10_5 { + flags: CapabilitiesV104Flags::AVC_DISABLED, + }, + CapabilitySet::V10_6 { + flags: CapabilitiesV104Flags::AVC_DISABLED, + }, + ]) +}); +pub static FRAME_ACKNOWLEDGE: LazyLock = LazyLock::new(|| FrameAcknowledgePdu { + queue_depth: QueueDepth::Unavailable, + frame_id: 1, + total_frames_decoded: 1, +}); +pub static CACHE_IMPORT_REPLY: LazyLock = LazyLock::new(|| CacheImportReplyPdu { + cache_slots: vec![ + 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, + 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, + 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, + 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, + 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, + 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, + 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, + 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, + 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, + 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 0x100, + 0x101, 0x102, 0x103, 0x104, 0x105, 0x106, 0x107, 0x108, 0x109, 0x10a, 0x10b, 0x10c, 0x10d, 0x10e, 0x10f, 0x110, + 0x111, 0x112, 0x113, 0x114, 0x115, 0x116, 0x117, 0x118, 0x119, 0x11a, 0x11b, 0x11c, 0x11d, 0x11e, 0x11f, 0x120, + 0x121, 0x122, 0x123, 0x124, 0x125, 0x126, 0x127, 0x128, 0x129, 0x12a, 0x12b, 0x12c, 0x12d, 0x12e, 0x12f, 0x130, + 0x131, 0x132, 0x133, 0x134, 0x135, 0x136, 0x137, 0x138, 0x139, 0x13a, 0x13b, 0x13c, 0x13d, 0x13e, 0x13f, 0x140, + 0x141, 0x142, 0x143, 0x144, 0x145, 0x146, 0x147, 0x148, 0x149, 0x14a, 0x14b, 0x14c, 0x14d, 0x14e, 0x14f, 0x150, + 0x151, 0x152, 0x153, 0x154, 0x155, 0x156, 0x157, 0x158, 0x159, 0x15a, 0x15b, 0x15c, 0x15d, 0x15e, 0x15f, 0x160, + 0x161, 0x162, 0x163, 0x164, 0x165, 0x166, 0x167, 0x168, 0x169, 0x16a, 0x16b, 0x16c, 0x16d, 0x16e, 0x16f, 0x170, + 0x171, 0x172, 0x173, 0x174, 0x175, 0x176, 0x177, 0x178, 0x179, 0x17a, 0x17b, 0x17c, 0x17d, 0x17e, 0x17f, 0x180, + 0x181, 0x182, 0x183, 0x184, 0x185, 0x186, 0x187, 0x188, 0x189, 0x18a, 0x18b, 0x18c, 0x18d, 0x18e, 0x18f, 0x190, + 0x191, 0x192, 0x193, 0x194, 0x195, 0x196, 0x197, 0x198, 0x199, 0x19a, 0x19b, 0x19c, 0x19d, 0x19e, 0x19f, 0x1a0, + 0x1a1, 0x1a2, 0x1a3, 0x1a4, 0x1a5, 0x1a6, 0x1a7, 0x1a8, 0x1a9, 0x1aa, 0x1ab, 0x1ac, 0x1ad, 0x1ae, 0x1af, 0x1b0, + 0x1b1, 0x1b2, 0x1b3, 0x1b4, 0x1b5, 0x1b6, 0x1b7, 0x1b8, 0x1b9, 0x1ba, 0x1bb, 0x1bc, 0x1bd, 0x1be, 0x1bf, 0x1c0, + 0x1c1, 0x1c2, 0x1c3, 0x1c4, 0x1c5, 0x1c6, 0x1c7, 0x1c8, 0x1c9, 0x1ca, 0x1cb, 0x1cc, 0x1cd, 0x1ce, 0x1cf, 0x1d0, + 0x1d1, 0x1d2, 0x1d3, 0x1d4, 0x1d5, 0x1d6, 0x1d7, 0x1d8, 0x1d9, 0x1da, 0x1db, 0x1dc, 0x1dd, 0x1de, 0x1df, 0x1e0, + 0x1e1, 0x1e2, 0x1e3, 0x1e4, 0x1e5, 0x1e6, 0x1e7, 0x1e8, 0x1e9, 0x1ea, 0x1eb, 0x1ec, 0x1ed, 0x1ee, 0x1ef, 0x1f0, + 0x1f1, 0x1f2, 0x1f3, 0x1f4, 0x1f5, 0x1f6, 0x1f7, 0x1f8, 0x1f9, 0x1fa, 0x1fb, 0x1fc, 0x1fd, 0x1fe, 0x1ff, 0x200, + 0x201, 0x202, 0x203, 0x204, 0x205, 0x206, 0x207, 0x208, 0x209, 0x20a, 0x20b, 0x20c, 0x20d, 0x20e, 0x20f, 0x210, + 0x211, 0x212, 0x213, 0x214, 0x215, 0x216, 0x217, 0x218, 0x219, 0x21a, 0x21b, 0x21c, 0x21d, 0x21e, 0x21f, 0x220, + 0x221, 0x222, 0x223, 0x224, 0x225, 0x226, 0x227, 0x228, 0x229, 0x22a, 0x22b, 0x22c, 0x22d, 0x22e, 0x22f, 0x230, + 0x231, 0x232, 0x233, 0x234, 0x235, 0x236, 0x237, 0x238, 0x239, 0x23a, 0x23b, 0x23c, 0x23d, 0x23e, 0x23f, 0x240, + 0x241, 0x242, 0x243, 0x244, 0x245, 0x246, 0x247, 0x248, 0x249, 0x24a, 0x24b, 0x24c, 0x24d, 0x24e, 0x24f, 0x250, + 0x251, 0x252, 0x253, 0x254, 0x255, 0x256, 0x257, 0x258, 0x259, 0x25a, 0x25b, 0x25c, 0x25d, 0x25e, 0x25f, 0x260, + 0x261, 0x262, 0x263, 0x264, 0x265, 0x266, 0x267, 0x268, 0x269, 0x26a, 0x26b, 0x26c, 0x26d, 0x26e, 0x26f, 0x270, + 0x271, 0x272, 0x273, 0x274, 0x275, 0x276, 0x277, 0x278, 0x279, 0x27a, 0x27b, 0x27c, 0x27d, 0x27e, 0x27f, 0x280, + 0x281, 0x282, 0x283, 0x284, 0x285, 0x286, 0x287, 0x288, 0x289, 0x28a, 0x28b, 0x28c, 0x28d, 0x28e, 0x28f, 0x290, + 0x291, 0x292, 0x293, 0x294, 0x295, 0x296, 0x297, 0x298, 0x299, 0x29a, 0x29b, 0x29c, 0x29d, 0x29e, 0x29f, 0x2a0, + 0x2a1, 0x2a2, 0x2a3, 0x2a4, 0x2a5, 0x2a6, 0x2a7, 0x2a8, 0x2a9, 0x2aa, 0x2ab, 0x2ac, 0x2ad, 0x2ae, 0x2af, 0x2b0, + 0x2b1, 0x2b2, 0x2b3, 0x2b4, 0x2b5, 0x2b6, 0x2b7, 0x2b8, 0x2b9, 0x2ba, 0x2bb, 0x2bc, 0x2bd, 0x2be, 0x2bf, 0x2c0, + 0x2c1, 0x2c2, 0x2c3, 0x2c4, 0x2c5, 0x2c6, 0x2c7, 0x2c8, 0x2c9, 0x2ca, 0x2cb, 0x2cc, 0x2cd, 0x2ce, 0x2cf, 0x2d0, + 0x2d1, 0x2d2, 0x2d3, 0x2d4, 0x2d5, 0x2d6, 0x2d7, 0x2d8, 0x2d9, 0x2da, 0x2db, 0x2dc, 0x2dd, 0x2de, 0x2df, 0x2e0, + 0x2e1, 0x2e2, 0x2e3, 0x2e4, 0x2e5, 0x2e6, 0x2e7, 0x2e8, 0x2e9, 0x2ea, 0x2eb, 0x2ec, 0x2ed, 0x2ee, 0x2ef, 0x2f0, + 0x2f1, 0x2f2, 0x2f3, 0x2f4, 0x2f5, 0x2f6, 0x2f7, 0x2f8, 0x2f9, 0x2fa, 0x2fb, 0x2fc, 0x2fd, 0x2fe, 0x2ff, 0x300, + 0x301, 0x302, 0x303, 0x304, 0x305, 0x306, 0x307, 0x308, 0x309, 0x30a, 0x30b, 0x30c, 0x30d, 0x30e, 0x30f, 0x310, + 0x311, 0x312, 0x313, 0x314, 0x315, 0x316, 0x317, 0x318, 0x319, 0x31a, 0x31b, 0x31c, 0x31d, 0x31e, 0x31f, 0x320, + 0x321, 0x322, 0x323, 0x324, 0x325, 0x326, 0x327, 0x328, 0x329, 0x32a, 0x32b, 0x32c, 0x32d, 0x32e, 0x32f, 0x330, + 0x331, 0x332, 0x333, 0x334, 0x335, 0x336, 0x337, 0x338, 0x339, 0x33a, 0x33b, 0x33c, 0x33d, 0x33e, 0x33f, 0x340, + 0x341, 0x342, 0x343, 0x344, 0x345, 0x346, 0x347, 0x348, 0x349, 0x34a, 0x34b, 0x34c, 0x34d, 0x34e, 0x34f, 0x350, + 0x351, 0x352, 0x353, 0x354, 0x355, 0x356, 0x357, 0x358, 0x359, 0x35a, 0x35b, 0x35c, 0x35d, 0x35e, 0x35f, 0x360, + 0x361, 0x362, 0x363, 0x364, 0x365, 0x366, 0x367, 0x368, 0x369, 0x36a, 0x36b, 0x36c, 0x36d, 0x36e, 0x36f, 0x370, + 0x371, 0x372, 0x373, 0x374, 0x375, 0x376, 0x377, 0x378, 0x379, 0x37a, 0x37b, 0x37c, 0x37d, 0x37e, 0x37f, 0x380, + 0x381, 0x382, 0x383, 0x384, 0x385, 0x386, 0x387, 0x388, 0x389, 0x38a, 0x38b, 0x38c, 0x38d, 0x38e, 0x38f, 0x390, + 0x391, 0x392, 0x393, 0x394, 0x395, 0x396, 0x397, 0x398, + ], +}); +pub static AVC_444_BITMAP: LazyLock> = LazyLock::new(|| Avc444BitmapStream { + encoding: Encoding::CHROMA, + stream1: Avc420BitmapStream { + rectangles: vec![InclusiveRectangle { + left: 1792, + top: 1056, + right: 1808, + bottom: 1072, + }], + quant_qual_vals: vec![QuantQuality { + quantization_parameter: 22, + progressive: false, + quality: 100, + }], + data: &AVC_444_MESSAGE_CORRECT_LEN[18..], + }, + stream2: None, +}); diff --git a/crates/ironrdp-testsuite-core/src/lib.rs b/crates/ironrdp-testsuite-core/src/lib.rs new file mode 100644 index 00000000..3318fa49 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/lib.rs @@ -0,0 +1,30 @@ +// No need to be as strict as in production libraries +#![allow(clippy::arithmetic_side_effects)] +#![allow(clippy::cast_lossless)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_possible_wrap)] +#![allow(clippy::cast_sign_loss)] +#![allow(unused_crate_dependencies)] +#![allow(clippy::unwrap_used, reason = "unwrap is fine in tests")] + +mod macros; + +pub mod capsets; +pub mod client_info; +pub mod cluster_data; +pub mod conference_create; +pub mod core_data; +pub mod gcc; +pub mod gfx; +pub mod graphics_messages; +pub mod mcs; +pub mod message_channel_data; +pub mod monitor_data; +pub mod monitor_extended_data; +pub mod multi_transport_channel_data; +pub mod network_data; +pub mod rdp; +pub mod security_data; + +#[doc(hidden)] +pub use paste::paste; diff --git a/crates/ironrdp-testsuite-core/src/macros.rs b/crates/ironrdp-testsuite-core/src/macros.rs new file mode 100644 index 00000000..30de32b1 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/macros.rs @@ -0,0 +1,170 @@ +/// Same macro as in `assert_hex` crate, but use `{:02X?}` instead of `{:#x}` because the alternate formatting +/// for slice / Vec is inserting a newline between each element which is not very readable for binary payloads. +/// +/// [Original macro](https://docs.rs/assert_hex/latest/src/assert_hex/lib.rs.html#19). +#[macro_export] +macro_rules! assert_eq_hex { + ($left:expr, $right:expr $(,)?) => ({ + match (&$left, &$right) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + // The reborrows below are intentional. Without them, the stack slot for the + // borrow is initialized even before the values are compared, leading to a + // noticeable slow down. + panic!(r#"assertion failed: `(left == right)` + left: `{:02X?}`, + right: `{:02X?}`"#, &*left_val, &*right_val) + } + } + } + }); + ($left:expr, $right:expr, $($arg:tt)+) => ({ + match (&($left), &($right)) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + // The reborrows below are intentional. Without them, the stack slot for the + // borrow is initialized even before the values are compared, leading to a + // noticeable slow down. + panic!(r#"assertion failed: `(left == right)` + left: `{:02X?}`, + right: `{:02X?}`: {}"#, &*left_val, &*right_val, + format_args!($($arg)+)) + } + } + } + }); +} + +/// Same macro as in `assert_hex` crate, but use `{:02X?}` instead of `{:#x}` because the alternate formatting +/// for slice / Vec is inserting a newline between each element which is not very readable for binary payloads. +/// +/// [Original macro](https://docs.rs/assert_hex/latest/src/assert_hex/lib.rs.html#56). +#[macro_export] +macro_rules! assert_ne_hex { + ($left:expr, $right:expr $(,)?) => ({ + match (&$left, &$right) { + (left_val, right_val) => { + if *left_val == *right_val { + // The reborrows below are intentional. Without them, the stack slot for the + // borrow is initialized even before the values are compared, leading to a + // noticeable slow down. + panic!(r#"assertion failed: `(left != right)` + left: `{:02X?}`, + right: `{:02X?}`"#, &*left_val, &*right_val) + } + } + } + }); + ($left:expr, $right:expr, $($arg:tt)+) => ({ + match (&($left), &($right)) { + (left_val, right_val) => { + if *left_val == *right_val { + // The reborrows below are intentional. Without them, the stack slot for the + // borrow is initialized even before the values are compared, leading to a + // noticeable slow down. + panic!(r#"assertion failed: `(left != right)` + left: `{:02X?}`, + right: `{:02X?}`: {}"#, &*left_val, &*right_val, + format_args!($($arg)+)) + } + } + } + }); +} + +#[macro_export] +macro_rules! encode_decode_test { + ($test_name:ident : $pdu:expr , $encoded_pdu:expr) => { + $crate::paste! { + #[test] + fn [< $test_name _encode >]() { + let pdu = $pdu; + let expected = $encoded_pdu; + + let encoded = ::ironrdp_core::encode_vec(&pdu).unwrap(); + + $crate::assert_eq_hex!(encoded, expected); + } + + #[test] + fn [< $test_name _decode >]() { + let encoded = $encoded_pdu; + let expected = $pdu; + + let decoded = ::ironrdp_core::decode(&encoded).unwrap(); + + let _ = expected == decoded; // type inference trick + + $crate::assert_eq_hex!(decoded, expected); + } + + #[test] + fn [< $test_name _size >]() { + let pdu = $pdu; + let expected = $encoded_pdu.len(); + + let pdu_size = ::ironrdp_core::size(&pdu); + + $crate::assert_eq_hex!(pdu_size, expected); + } + } + }; + ($( $test_name:ident : $pdu:expr , $encoded_pdu:expr ; )+) => { + $( + $crate::encode_decode_test!($test_name: $pdu, $encoded_pdu); + )+ + }; +} + +#[macro_export] +macro_rules! mcs_encode_decode_test { + ($test_name:ident : $pdu:expr , $encoded_pdu:expr) => { + $crate::paste! { + #[test] + fn [< $test_name _encode >]() { + use ::ironrdp_pdu::mcs::McsPdu; + + let pdu = $pdu; + let expected = $encoded_pdu; + + let mut encoded = vec![0; expected.len()]; + let mut cursor = ::ironrdp_core::WriteCursor::new(&mut encoded); + pdu.mcs_body_encode(&mut cursor).unwrap(); + + $crate::assert_eq_hex!(encoded, expected); + } + + #[test] + fn [< $test_name _decode >]() { + use ::ironrdp_pdu::mcs::McsPdu; + + let encoded = $encoded_pdu; + let expected = $pdu; + + let mut cursor = ::ironrdp_core::ReadCursor::new(&encoded); + let decoded = McsPdu::mcs_body_decode(&mut cursor, encoded.len()).unwrap(); + + let _ = expected == decoded; // type inference trick + + $crate::assert_eq_hex!(decoded, expected); + } + + #[test] + fn [< $test_name _size >]() { + use ::ironrdp_pdu::mcs::McsPdu; + + let pdu = $pdu; + let expected = $encoded_pdu.len(); + + let pdu_size = pdu.mcs_size(); + + $crate::assert_eq_hex!(pdu_size, expected); + } + } + }; + ($( $test_name:ident : $pdu:expr , $encoded_pdu:expr ; )+) => { + $( + $crate::mcs_encode_decode_test!($test_name: $pdu, $encoded_pdu); + )+ + }; +} diff --git a/crates/ironrdp-testsuite-core/src/mcs.rs b/crates/ironrdp-testsuite-core/src/mcs.rs new file mode 100644 index 00000000..5836945d --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/mcs.rs @@ -0,0 +1,157 @@ +use std::borrow::Cow; +use std::sync::LazyLock; + +use array_concat::{concat_arrays, concat_arrays_size}; +use ironrdp_pdu::mcs::{ + AttachUserConfirm, AttachUserRequest, ChannelJoinConfirm, ChannelJoinRequest, ConnectInitial, ConnectResponse, + DisconnectProviderUltimatum, DisconnectReason, DomainParameters, ErectDomainPdu, OwnedSendDataIndication, + OwnedSendDataRequest, SendDataIndication, SendDataRequest, +}; + +use crate::conference_create::{ + CONFERENCE_CREATE_REQUEST, CONFERENCE_CREATE_REQUEST_BUFFER, CONFERENCE_CREATE_RESPONSE, + CONFERENCE_CREATE_RESPONSE_BUFFER, +}; +use crate::rdp::{CLIENT_INFO_PDU_BUFFER, SERVER_LICENSE_BUFFER}; + +pub const ERECT_DOMAIN_PDU_BUFFER: [u8; 5] = [0x04, 0x01, 0x00, 0x01, 0x00]; + +pub const ERECT_DOMAIN_PDU: ErectDomainPdu = ErectDomainPdu { + sub_height: 0, + sub_interval: 0, +}; + +pub const ATTACH_USER_REQUEST_PDU_BUFFER: [u8; 1] = [0x28]; + +pub const ATTACH_USER_REQUEST_PDU: AttachUserRequest = AttachUserRequest; + +pub const ATTACH_USER_CONFIRM_PDU_BUFFER: [u8; 4] = [0x2e, 0x00, 0x00, 0x06]; + +pub const ATTACH_USER_CONFIRM_PDU: AttachUserConfirm = AttachUserConfirm { + result: 0, + initiator_id: 1007, +}; + +pub const CHANNEL_JOIN_REQUEST_PDU_BUFFER: [u8; 5] = [0x38, 0x00, 0x06, 0x03, 0xef]; + +pub const CHANNEL_JOIN_REQUEST_PDU: ChannelJoinRequest = ChannelJoinRequest { + initiator_id: 1007, + channel_id: 1007, +}; + +pub const CHANNEL_JOIN_CONFIRM_PDU_BUFFER: [u8; 8] = [0x3e, 0x00, 0x00, 0x06, 0x03, 0xef, 0x03, 0xef]; + +pub const CHANNEL_JOIN_CONFIRM_PDU: ChannelJoinConfirm = ChannelJoinConfirm { + result: 0, + initiator_id: 1007, + requested_channel_id: 1007, + channel_id: 1007, +}; + +pub const DISCONNECT_PROVIDER_ULTIMATUM_PDU_BUFFER: [u8; 2] = [0x21, 0x80]; + +pub const DISCONNECT_PROVIDER_ULTIMATUM_PDU: DisconnectProviderUltimatum = DisconnectProviderUltimatum { + reason: DisconnectReason::UserRequested, +}; + +pub const SEND_DATA_REQUEST_PDU_BUFFER_PREFIX: [u8; 8] = [0x64, 0x00, 0x06, 0x03, 0xeb, 0x70, 0x81, 0x92]; + +pub const SEND_DATA_REQUEST_PDU_BUFFER: [u8; concat_arrays_size!( + SEND_DATA_REQUEST_PDU_BUFFER_PREFIX, + CLIENT_INFO_PDU_BUFFER +)] = concat_arrays!(SEND_DATA_REQUEST_PDU_BUFFER_PREFIX, CLIENT_INFO_PDU_BUFFER); + +pub const SEND_DATA_REQUEST_PDU: OwnedSendDataRequest = SendDataRequest { + initiator_id: 1007, + channel_id: 1003, + user_data: Cow::Borrowed(&CLIENT_INFO_PDU_BUFFER), +}; + +pub const SEND_DATA_INDICATION_PDU_BUFFER_PREFIX: [u8; 7] = [0x68, 0x00, 0x01, 0x03, 0xeb, 0x70, 0x14]; + +pub const SEND_DATA_INDICATION_PDU_BUFFER: [u8; concat_arrays_size!( + SEND_DATA_INDICATION_PDU_BUFFER_PREFIX, + SERVER_LICENSE_BUFFER +)] = concat_arrays!(SEND_DATA_INDICATION_PDU_BUFFER_PREFIX, SERVER_LICENSE_BUFFER); + +pub const SEND_DATA_INDICATION_PDU: OwnedSendDataIndication = SendDataIndication { + initiator_id: 1002, + channel_id: 1003, + user_data: Cow::Borrowed(&SERVER_LICENSE_BUFFER), +}; + +pub const CONNECT_INITIAL_PREFIX_BUFFER: [u8; 107] = [ + 0x7f, 0x65, 0x82, 0x01, 0x99, 0x04, 0x01, 0x01, 0x04, 0x01, 0x01, 0x01, 0x01, 0xff, 0x30, 0x1a, 0x02, 0x01, 0x22, + 0x02, 0x01, 0x02, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x03, 0x00, 0xff, + 0xff, 0x02, 0x01, 0x02, 0x30, 0x19, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, + 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x02, 0x04, 0x20, 0x02, 0x01, 0x02, 0x30, 0x20, 0x02, 0x03, 0x00, 0xff, 0xff, + 0x02, 0x03, 0x00, 0xfc, 0x17, 0x02, 0x03, 0x00, 0xff, 0xff, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, + 0x02, 0x03, 0x00, 0xff, 0xff, 0x02, 0x01, 0x02, 0x04, 0x82, 0x01, 0x33, +]; + +pub const CONNECT_RESPONSE_PREFIX_BUFFER: [u8; 43] = [ + 0x7f, 0x66, 0x82, 0x01, 0x46, 0x0a, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x1a, 0x02, 0x01, 0x22, 0x02, 0x01, 0x03, + 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x03, 0x00, 0xff, 0xf8, 0x02, 0x01, + 0x02, 0x04, 0x82, 0x01, 0x20, +]; + +pub const CONNECT_INITIAL_BUFFER: [u8; concat_arrays_size!( + CONNECT_INITIAL_PREFIX_BUFFER, + CONFERENCE_CREATE_REQUEST_BUFFER +)] = concat_arrays!(CONNECT_INITIAL_PREFIX_BUFFER, CONFERENCE_CREATE_REQUEST_BUFFER); + +pub const CONNECT_RESPONSE_BUFFER: [u8; concat_arrays_size!( + CONNECT_RESPONSE_PREFIX_BUFFER, + CONFERENCE_CREATE_RESPONSE_BUFFER +)] = concat_arrays!(CONNECT_RESPONSE_PREFIX_BUFFER, CONFERENCE_CREATE_RESPONSE_BUFFER); + +pub static CONNECT_INITIAL: LazyLock = LazyLock::new(|| ConnectInitial { + calling_domain_selector: vec![0x01], + called_domain_selector: vec![0x01], + upward_flag: true, + target_parameters: DomainParameters { + max_channel_ids: 34, + max_user_ids: 2, + max_token_ids: 0, + num_priorities: 1, + min_throughput: 0, + max_height: 1, + max_mcs_pdu_size: 65535, + protocol_version: 2, + }, + min_parameters: DomainParameters { + max_channel_ids: 1, + max_user_ids: 1, + max_token_ids: 1, + num_priorities: 1, + min_throughput: 0, + max_height: 1, + max_mcs_pdu_size: 1056, + protocol_version: 2, + }, + max_parameters: DomainParameters { + max_channel_ids: 65535, + max_user_ids: 64535, + max_token_ids: 65535, + num_priorities: 1, + min_throughput: 0, + max_height: 1, + max_mcs_pdu_size: 65535, + protocol_version: 2, + }, + conference_create_request: CONFERENCE_CREATE_REQUEST.clone(), +}); +pub static CONNECT_RESPONSE: LazyLock = LazyLock::new(|| ConnectResponse { + called_connect_id: 0, + domain_parameters: DomainParameters { + max_channel_ids: 34, + max_user_ids: 3, + max_token_ids: 0, + num_priorities: 1, + min_throughput: 0, + max_height: 1, + max_mcs_pdu_size: 65528, + protocol_version: 2, + }, + conference_create_response: CONFERENCE_CREATE_RESPONSE.clone(), +}); diff --git a/crates/ironrdp-testsuite-core/src/message_channel_data.rs b/crates/ironrdp-testsuite-core/src/message_channel_data.rs new file mode 100644 index 00000000..e85fb21c --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/message_channel_data.rs @@ -0,0 +1,7 @@ +use ironrdp_pdu::gcc::ServerMessageChannelData; + +pub const SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER: [u8; 2] = [0xf0, 0x03]; + +pub const SERVER_GCC_MESSAGE_CHANNEL_BLOCK: ServerMessageChannelData = ServerMessageChannelData { + mcs_message_channel_id: 0x03f0, +}; diff --git a/crates/ironrdp-testsuite-core/src/monitor_data.rs b/crates/ironrdp-testsuite-core/src/monitor_data.rs new file mode 100644 index 00000000..1bd15bc0 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/monitor_data.rs @@ -0,0 +1,32 @@ +use std::sync::LazyLock; + +use ironrdp_pdu::gcc::{ClientMonitorData, Monitor, MonitorFlags}; + +pub const MONITOR_DATA_WITHOUT_MONITORS_BUFFER: [u8; 8] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + +pub const MONITOR_DATA_WITH_MONITORS_BUFFER: [u8; 48] = [ + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x07, 0x00, + 0x00, 0x37, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub static MONITOR_DATA_WITHOUT_MONITORS: LazyLock = + LazyLock::new(|| ClientMonitorData { monitors: Vec::new() }); +pub static MONITOR_DATA_WITH_MONITORS: LazyLock = LazyLock::new(|| ClientMonitorData { + monitors: vec![ + Monitor { + left: 0, + top: 0, + right: 1919, + bottom: 1079, + flags: MonitorFlags::PRIMARY, + }, + Monitor { + left: -1280, + top: 0, + right: -1, + bottom: 1023, + flags: MonitorFlags::empty(), + }, + ], +}); diff --git a/crates/ironrdp-testsuite-core/src/monitor_extended_data.rs b/crates/ironrdp-testsuite-core/src/monitor_extended_data.rs new file mode 100644 index 00000000..089f9679 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/monitor_extended_data.rs @@ -0,0 +1,36 @@ +use std::sync::LazyLock; + +use ironrdp_pdu::gcc::{ClientMonitorExtendedData, ExtendedMonitorInfo, MonitorOrientation}; + +pub const MONITOR_DATA_WITHOUT_MONITORS_BUFFER: [u8; 12] = + [0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + +pub const MONITOR_DATA_WITH_MONITORS_BUFFER: [u8; 52] = [ + 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub static MONITOR_DATA_WITHOUT_MONITORS: LazyLock = + LazyLock::new(|| ClientMonitorExtendedData { + extended_monitors_info: Vec::new(), + }); +pub static MONITOR_DATA_WITH_MONITORS: LazyLock = + LazyLock::new(|| ClientMonitorExtendedData { + extended_monitors_info: vec![ + ExtendedMonitorInfo { + physical_width: 0, + physical_height: 0, + orientation: MonitorOrientation::Landscape, + desktop_scale_factor: 0, + device_scale_factor: 0, + }, + ExtendedMonitorInfo { + physical_width: 0, + physical_height: 0, + orientation: MonitorOrientation::Landscape, + desktop_scale_factor: 0, + device_scale_factor: 0, + }, + ], + }); diff --git a/crates/ironrdp-testsuite-core/src/multi_transport_channel_data.rs b/crates/ironrdp-testsuite-core/src/multi_transport_channel_data.rs new file mode 100644 index 00000000..c950d891 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/multi_transport_channel_data.rs @@ -0,0 +1,12 @@ +use std::sync::LazyLock; + +use ironrdp_pdu::gcc::{MultiTransportChannelData, MultiTransportFlags}; + +pub const SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER: [u8; 4] = [0x01, 0x03, 0x00, 0x00]; + +pub static SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK: LazyLock = + LazyLock::new(|| MultiTransportChannelData { + flags: MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR + | MultiTransportFlags::TRANSPORT_TYPE_UDP_PREFERRED + | MultiTransportFlags::SOFT_SYNC_TCP_TO_UDP, + }); diff --git a/crates/ironrdp-testsuite-core/src/network_data.rs b/crates/ironrdp-testsuite-core/src/network_data.rs new file mode 100644 index 00000000..ad8b4196 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/network_data.rs @@ -0,0 +1,61 @@ +use std::sync::LazyLock; + +use ironrdp_pdu::gcc::{ChannelDef, ChannelName, ChannelOptions, ClientNetworkData, ServerNetworkData}; + +pub const CLIENT_NETWORK_DATA_WITH_CHANNELS_BUFFER: [u8; 40] = [ + 0x03, 0x00, 0x00, 0x00, // channels count + 0x72, 0x64, 0x70, 0x64, 0x72, 0x00, 0x00, 0x00, // channel 1::name + 0x00, 0x00, 0x80, 0x80, // channel 1::options + 0x63, 0x6c, 0x69, 0x70, 0x72, 0x64, 0x72, 0x00, // channel 2::name + 0x00, 0x00, 0xa0, 0xc0, // channel 2::options + 0x72, 0x64, 0x70, 0x73, 0x6e, 0x64, 0x00, 0x00, // channel 3::name + 0x00, 0x00, 0x00, 0xc0, // channel 3::options +]; + +pub const SERVER_NETWORK_DATA_WITH_CHANNELS_ID_BUFFER: [u8; 12] = [ + 0xeb, 0x03, // io channel + 0x03, 0x00, // channels count + 0xec, 0x03, // channel 1::id + 0xed, 0x03, // channel 2::id + 0xee, 0x03, // channel 3::id + 0x00, 0x00, // padding +]; + +pub const CLIENT_NETWORK_DATA_WITHOUT_CHANNELS_BUFFER: [u8; 4] = [ + 0x00, 0x00, 0x00, 0x00, // channels count +]; + +pub const SERVER_NETWORK_DATA_WITHOUT_CHANNELS_ID_BUFFER: [u8; 4] = [ + 0xeb, 0x03, // io channel + 0x00, 0x00, // channels count +]; + +pub static CLIENT_NETWORK_DATA_WITH_CHANNELS: LazyLock = LazyLock::new(|| ClientNetworkData { + channels: vec![ + ChannelDef { + name: ChannelName::from_utf8("rdpdr").unwrap(), + options: ChannelOptions::INITIALIZED | ChannelOptions::COMPRESS_RDP, + }, + ChannelDef { + name: ChannelName::from_utf8("cliprdr").unwrap(), + options: ChannelOptions::INITIALIZED + | ChannelOptions::COMPRESS_RDP + | ChannelOptions::ENCRYPT_RDP + | ChannelOptions::SHOW_PROTOCOL, + }, + ChannelDef { + name: ChannelName::from_utf8("rdpsnd").unwrap(), + options: ChannelOptions::INITIALIZED | ChannelOptions::ENCRYPT_RDP, + }, + ], +}); +pub static SERVER_NETWORK_DATA_WITH_CHANNELS_ID: LazyLock = LazyLock::new(|| ServerNetworkData { + io_channel: 1003, + channel_ids: vec![1004, 1005, 1006], +}); +pub static CLIENT_NETWORK_DATA_WITHOUT_CHANNELS: LazyLock = + LazyLock::new(|| ClientNetworkData { channels: Vec::new() }); +pub static SERVER_NETWORK_DATA_WITHOUT_CHANNELS_ID: LazyLock = LazyLock::new(|| ServerNetworkData { + io_channel: 1003, + channel_ids: Vec::new(), +}); diff --git a/crates/ironrdp-testsuite-core/src/rdp.rs b/crates/ironrdp-testsuite-core/src/rdp.rs new file mode 100644 index 00000000..ae4a91a7 --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/rdp.rs @@ -0,0 +1,313 @@ +use std::sync::LazyLock; + +use array_concat::{concat_arrays, concat_arrays_size}; +use ironrdp_pdu::gcc; +use ironrdp_pdu::rdp::finalization_messages::{ + ControlAction, ControlPdu, FontPdu, MonitorLayoutPdu, SequenceFlags, SynchronizePdu, +}; +use ironrdp_pdu::rdp::headers::{ + BasicSecurityHeader, BasicSecurityHeaderFlags, CompressionFlags, ShareControlHeader, ShareControlPdu, + ShareDataHeader, ShareDataPdu, StreamPriority, +}; +use ironrdp_pdu::rdp::server_license::{ + LicenseErrorCode, LicenseHeader, LicensePdu, LicensingErrorMessage, LicensingStateTransition, PreambleFlags, + PreambleType, PreambleVersion, +}; +use ironrdp_pdu::rdp::{client_info, ClientInfoPdu}; + +use crate::capsets::{ + CLIENT_DEMAND_ACTIVE, CLIENT_DEMAND_ACTIVE_BUFFER, SERVER_DEMAND_ACTIVE, SERVER_DEMAND_ACTIVE_BUFFER, +}; +use crate::client_info::{CLIENT_INFO_BUFFER_UNICODE, CLIENT_INFO_UNICODE}; +use crate::monitor_data::MONITOR_DATA_WITH_MONITORS_BUFFER; + +pub const CLIENT_INFO_PDU_SECURITY_HEADER_BUFFER: [u8; 4] = [ + 0x40, 0x00, // flags + 0x00, 0x00, // flagsHi +]; + +pub const SERVER_DEMAND_ACTIVE_PDU_HEADERS_BUFFER: [u8; 10] = [ + 0x6f, 0x01, // ShareControlHeader::totalLength + 0x11, 0x00, // ShareControlHeader::pduType + 0xea, 0x03, // ShareControlHeader::PduSource + 0xea, 0x03, 0x01, 0x00, // share id +]; + +pub const CLIENT_DEMAND_ACTIVE_PDU_HEADERS_BUFFER: [u8; 10] = [ + 0xf0, 0x01, // ShareControlHeader::totalLength + 0x13, 0x00, // ShareControlHeader::pduType + 0xef, 0x03, // ShareControlHeader::PduSource + 0xea, 0x03, 0x01, 0x00, // share id +]; + +pub const MONITOR_LAYOUT_HEADERS_BUFFER: [u8; 18] = [ + 0x3e, 0x00, // ShareControlHeader::totalLength + 0x17, 0x00, // ShareControlHeader::pduType + 0xef, 0x03, // ShareControlHeader::PduSource + 0xea, 0x03, 0x01, 0x00, // share id + 0x00, // padding + 0x01, // stream id + 0x30, 0x00, // uncompressed length + 0x37, // pdu type + 0x00, // compression type + 0x00, 0x00, // compressed length +]; + +pub const CLIENT_SYNCHRONIZE_BUFFER: [u8; 22] = [ + 0x16, 0x00, // ShareControlHeader::totalLength + 0x17, 0x00, // ShareControlHeader::pduType + 0xef, 0x03, // ShareControlHeader::PduSource + 0xea, 0x03, 0x01, 0x00, // share id + 0x00, // padding + 0x01, // stream id + 0x08, 0x00, // uncompressed length + 0x1f, // pdu type + 0x00, // compression type + 0x00, 0x00, // compressed length + 0x01, 0x00, // message type + 0xea, 0x03, // target user +]; + +pub const CONTROL_COOPERATE_BUFFER: [u8; 26] = [ + 0x1a, 0x00, // ShareControlHeader::totalLength + 0x17, 0x00, // ShareControlHeader::pduType + 0xef, 0x03, // ShareControlHeader::PduSource + 0xea, 0x03, 0x01, 0x00, // share id + 0x00, // padding + 0x01, // stream id + 0x0c, 0x00, // uncompressed length + 0x14, // pdu type + 0x00, // compression type + 0x00, 0x00, // compressed length + 0x04, 0x00, // action + 0x00, 0x00, // grant id + 0x00, 0x00, 0x00, 0x00, // control id +]; + +pub const CONTROL_REQUEST_CONTROL_BUFFER: [u8; 26] = [ + 0x1a, 0x00, // ShareControlHeader::totalLength + 0x17, 0x00, // ShareControlHeader::pduType + 0xef, 0x03, // ShareControlHeader::PduSource + 0xea, 0x03, 0x01, 0x00, // share id + 0x00, // padding + 0x01, // stream id + 0x0c, 0x00, // uncompressed length + 0x14, // pdu type + 0x00, // compression type + 0x00, 0x00, // compressed length + 0x01, 0x00, // action + 0x00, 0x00, // grant id + 0x00, 0x00, 0x00, 0x00, // control id +]; + +pub const SERVER_GRANTED_CONTROL_BUFFER: [u8; 26] = [ + 0x1a, 0x00, // ShareControlHeader::totalLength + 0x17, 0x00, // ShareControlHeader::pduType + 0xea, 0x03, // ShareControlHeader::PduSource + 0xea, 0x03, 0x01, 0x00, // share id + 0x00, // padding + 0x02, // stream id + 0x0c, 0x00, // uncompressed length + 0x14, // pdu type + 0x00, // compression type + 0x00, 0x00, // compressed length + 0x02, 0x00, // action + 0xef, 0x03, // grant id + 0xea, 0x03, 0x00, 0x00, // control id +]; + +pub const CLIENT_FONT_LIST_BUFFER: [u8; 26] = [ + 0x1a, 0x00, // ShareControlHeader::totalLength + 0x17, 0x00, // ShareControlHeader::pduType + 0xef, 0x03, // ShareControlHeader::PduSource + 0xea, 0x03, 0x01, 0x00, // share id + 0x00, // padding + 0x01, // stream id + 0x0c, 0x00, // uncompressed length + 0x27, // pdu type + 0x00, // compression type + 0x00, 0x00, // compressed length + 0x00, 0x00, // number entries + 0x00, 0x00, // total number entries + 0x03, 0x00, // list flags + 0x32, 0x00, // entry size +]; + +pub const SERVER_FONT_MAP_BUFFER: [u8; 26] = [ + 0x1a, 0x00, // ShareControlHeader::totalLength + 0x17, 0x00, // ShareControlHeader::pduType + 0xea, 0x03, // ShareControlHeader::PduSource + 0xea, 0x03, 0x01, 0x00, // share id + 0x00, // padding + 0x02, // stream id + 0x0c, 0x00, // uncompressed length + 0x28, // pdu type + 0x00, // compression type + 0x00, 0x00, // compressed length + 0x00, 0x00, // number entries + 0x00, 0x00, // total number entries + 0x03, 0x00, // list flags + 0x04, 0x00, // entry size +]; + +pub const SERVER_LICENSE_BUFFER: [u8; 20] = [ + 0x80, 0x00, // flags + 0x00, 0x00, // flagsHi + 0xff, // preamble_message_type + 0x03, // preamble_flags | preamble_version + 0x14, // preamble_message_size + 0x00, 0x07, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, +]; + +pub static CLIENT_INFO_PDU: LazyLock = LazyLock::new(|| ClientInfoPdu { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::INFO_PKT, + }, + client_info: CLIENT_INFO_UNICODE.clone(), +}); +pub static SERVER_LICENSE_PDU: LazyLock = LazyLock::new(|| { + let mut pdu = LicensingErrorMessage { + license_header: LicenseHeader { + security_header: BasicSecurityHeader { + flags: BasicSecurityHeaderFlags::LICENSE_PKT, + }, + preamble_message_type: PreambleType::ErrorAlert, + preamble_flags: PreambleFlags::empty(), + preamble_version: PreambleVersion::V3, + preamble_message_size: 0, + }, + error_code: LicenseErrorCode::StatusValidClient, + state_transition: LicensingStateTransition::NoTransition, + error_info: Vec::new(), + }; + pdu.license_header.preamble_message_size = u16::try_from(pdu.size()).unwrap(); + pdu.into() +}); +pub static SERVER_DEMAND_ACTIVE_PDU: LazyLock = LazyLock::new(|| ShareControlHeader { + share_control_pdu: ShareControlPdu::ServerDemandActive(SERVER_DEMAND_ACTIVE.clone()), + pdu_source: 1002, + share_id: 66_538, +}); +pub static CLIENT_DEMAND_ACTIVE_PDU: LazyLock = LazyLock::new(|| ShareControlHeader { + share_control_pdu: ShareControlPdu::ClientConfirmActive(CLIENT_DEMAND_ACTIVE.clone()), + pdu_source: 1007, + share_id: 66_538, +}); +pub static CLIENT_SYNCHRONIZE: LazyLock = LazyLock::new(|| ShareControlHeader { + share_control_pdu: ShareControlPdu::Data(ShareDataHeader { + share_data_pdu: ShareDataPdu::Synchronize(SynchronizePdu { target_user_id: 0x03ea }), + stream_priority: StreamPriority::Low, + compression_flags: CompressionFlags::empty(), + compression_type: client_info::CompressionType::K8, + }), + pdu_source: 1007, + share_id: 66_538, +}); +pub static CONTROL_COOPERATE: LazyLock = LazyLock::new(|| ShareControlHeader { + share_control_pdu: ShareControlPdu::Data(ShareDataHeader { + share_data_pdu: ShareDataPdu::Control(ControlPdu { + action: ControlAction::Cooperate, + grant_id: 0, + control_id: 0, + }), + stream_priority: StreamPriority::Low, + compression_flags: CompressionFlags::empty(), + compression_type: client_info::CompressionType::K8, + }), + pdu_source: 1007, + share_id: 66_538, +}); +pub static CONTROL_REQUEST_CONTROL: LazyLock = LazyLock::new(|| ShareControlHeader { + share_control_pdu: ShareControlPdu::Data(ShareDataHeader { + share_data_pdu: ShareDataPdu::Control(ControlPdu { + action: ControlAction::RequestControl, + grant_id: 0, + control_id: 0, + }), + stream_priority: StreamPriority::Low, + compression_flags: CompressionFlags::empty(), + compression_type: client_info::CompressionType::K8, + }), + pdu_source: 1007, + share_id: 66_538, +}); +pub static SERVER_GRANTED_CONTROL: LazyLock = LazyLock::new(|| ShareControlHeader { + share_control_pdu: ShareControlPdu::Data(ShareDataHeader { + share_data_pdu: ShareDataPdu::Control(ControlPdu { + action: ControlAction::GrantedControl, + grant_id: 1007, + control_id: 1002, + }), + stream_priority: StreamPriority::Medium, + compression_flags: CompressionFlags::empty(), + compression_type: client_info::CompressionType::K8, + }), + pdu_source: 1002, + share_id: 66_538, +}); +pub static CLIENT_FONT_LIST: LazyLock = LazyLock::new(|| ShareControlHeader { + share_control_pdu: ShareControlPdu::Data(ShareDataHeader { + share_data_pdu: ShareDataPdu::FontList(FontPdu { + number: 0, + total_number: 0, + flags: SequenceFlags::FIRST | SequenceFlags::LAST, + entry_size: 50, + }), + stream_priority: StreamPriority::Low, + compression_flags: CompressionFlags::empty(), + compression_type: client_info::CompressionType::K8, + }), + pdu_source: 1007, + share_id: 66_538, +}); +pub static SERVER_FONT_MAP: LazyLock = LazyLock::new(|| ShareControlHeader { + share_control_pdu: ShareControlPdu::Data(ShareDataHeader { + share_data_pdu: ShareDataPdu::FontMap(FontPdu { + number: 0, + total_number: 0, + flags: SequenceFlags::FIRST | SequenceFlags::LAST, + entry_size: 4, + }), + stream_priority: StreamPriority::Medium, + compression_flags: CompressionFlags::empty(), + compression_type: client_info::CompressionType::K8, + }), + pdu_source: 1002, + share_id: 66_538, +}); +pub static MONITOR_LAYOUT_PDU: LazyLock = LazyLock::new(|| ShareControlHeader { + share_control_pdu: ShareControlPdu::Data(ShareDataHeader { + share_data_pdu: ShareDataPdu::MonitorLayout(MonitorLayoutPdu { + monitors: crate::monitor_data::MONITOR_DATA_WITH_MONITORS.monitors.clone(), + }), + stream_priority: StreamPriority::Low, + compression_flags: CompressionFlags::empty(), + compression_type: client_info::CompressionType::K8, + }), + pdu_source: 1007, + share_id: 66_538, +}); +pub static MONITOR_LAYOUT_PDU_BUFFER: LazyLock> = LazyLock::new(|| { + let mut buffer = MONITOR_LAYOUT_HEADERS_BUFFER.to_vec(); + buffer.extend( + MONITOR_DATA_WITH_MONITORS_BUFFER + .to_vec() + .split_off(gcc::MONITOR_FLAGS_SIZE), + ); + buffer +}); + +pub const CLIENT_INFO_PDU_BUFFER: [u8; concat_arrays_size!( + CLIENT_INFO_PDU_SECURITY_HEADER_BUFFER, + CLIENT_INFO_BUFFER_UNICODE +)] = concat_arrays!(CLIENT_INFO_PDU_SECURITY_HEADER_BUFFER, CLIENT_INFO_BUFFER_UNICODE); + +pub const SERVER_DEMAND_ACTIVE_PDU_BUFFER: [u8; concat_arrays_size!( + SERVER_DEMAND_ACTIVE_PDU_HEADERS_BUFFER, + SERVER_DEMAND_ACTIVE_BUFFER +)] = concat_arrays!(SERVER_DEMAND_ACTIVE_PDU_HEADERS_BUFFER, SERVER_DEMAND_ACTIVE_BUFFER); + +pub const CLIENT_DEMAND_ACTIVE_PDU_BUFFER: [u8; concat_arrays_size!( + CLIENT_DEMAND_ACTIVE_PDU_HEADERS_BUFFER, + CLIENT_DEMAND_ACTIVE_BUFFER +)] = concat_arrays!(CLIENT_DEMAND_ACTIVE_PDU_HEADERS_BUFFER, CLIENT_DEMAND_ACTIVE_BUFFER); diff --git a/crates/ironrdp-testsuite-core/src/security_data.rs b/crates/ironrdp-testsuite-core/src/security_data.rs new file mode 100644 index 00000000..87a65c8e --- /dev/null +++ b/crates/ironrdp-testsuite-core/src/security_data.rs @@ -0,0 +1,85 @@ +use std::sync::LazyLock; + +use array_concat::concat_arrays; +use ironrdp_pdu::gcc::{ClientSecurityData, EncryptionLevel, EncryptionMethod, ServerSecurityData}; + +pub const CLIENT_SECURITY_DATA_BUFFER: [u8; 8] = [ + 0x1b, 0x00, 0x00, 0x00, // encryption methods + 0x00, 0x00, 0x00, 0x00, // ext encryption methods +]; + +pub const SERVER_SECURITY_DATA_WITHOUT_OPTIONAL_FIELDS_BUFFER: [u8; 8] = [ + 0x00, 0x00, 0x00, 0x00, // encryption method + 0x00, 0x00, 0x00, 0x00, // encryption level +]; + +pub const SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_PREFIX_BUFFER: [u8; 8] = [ + 0x02, 0x00, 0x00, 0x00, // encryption method + 0x02, 0x00, 0x00, 0x00, // encryption level +]; + +pub const SERVER_RANDOM_BUFFER: [u8; 32] = [ + 0x10, 0x11, 0x77, 0x20, 0x30, 0x61, 0x0a, 0x12, 0xe4, 0x34, 0xa1, 0x1e, 0xf2, 0xc3, 0x9f, 0x31, 0x7d, 0xa4, 0x5f, + 0x01, 0x89, 0x34, 0x96, 0xe0, 0xff, 0x11, 0x08, 0x69, 0x7f, 0x1a, 0xc3, 0xd2, +]; + +pub const SERVER_CERT_BUFFER: [u8; 184] = [ + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x5c, 0x00, 0x52, 0x53, 0x41, + 0x31, 0x48, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xcb, 0x81, + 0xfe, 0xba, 0x6d, 0x61, 0xc3, 0x55, 0x05, 0xd5, 0x5f, 0x2e, 0x87, 0xf8, 0x71, 0x94, 0xd6, 0xf1, 0xa5, 0xcb, 0xf1, + 0x5f, 0x0c, 0x3d, 0xf8, 0x70, 0x02, 0x96, 0xc4, 0xfb, 0x9b, 0xc8, 0x3c, 0x2d, 0x55, 0xae, 0xe8, 0xff, 0x32, 0x75, + 0xea, 0x68, 0x79, 0xe5, 0xa2, 0x01, 0xfd, 0x31, 0xa0, 0xb1, 0x1f, 0x55, 0xa6, 0x1f, 0xc1, 0xf6, 0xd1, 0x83, 0x88, + 0x63, 0x26, 0x56, 0x12, 0xbc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x48, 0x00, 0xe9, 0xe1, + 0xd6, 0x28, 0x46, 0x8b, 0x4e, 0xf5, 0x0a, 0xdf, 0xfd, 0xee, 0x21, 0x99, 0xac, 0xb4, 0xe1, 0x8f, 0x5f, 0x81, 0x57, + 0x82, 0xef, 0x9d, 0x96, 0x52, 0x63, 0x27, 0x18, 0x29, 0xdb, 0xb3, 0x4a, 0xfd, 0x9a, 0xda, 0x42, 0xad, 0xb5, 0x69, + 0x21, 0x89, 0x0e, 0x1d, 0xc0, 0x4c, 0x1a, 0xa8, 0xaa, 0x71, 0x3e, 0x0f, 0x54, 0xb9, 0x9a, 0xe4, 0x99, 0x68, 0x3f, + 0x6c, 0xd6, 0x76, 0x84, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub static CLIENT_SECURITY_DATA: LazyLock = LazyLock::new(|| ClientSecurityData { + encryption_methods: EncryptionMethod::BIT_40 + | EncryptionMethod::BIT_128 + | EncryptionMethod::BIT_56 + | EncryptionMethod::FIPS, + ext_encryption_methods: 0, +}); +pub static SERVER_SECURITY_DATA_WITHOUT_OPTIONAL_FIELDS: LazyLock = + LazyLock::new(|| ServerSecurityData { + encryption_method: EncryptionMethod::empty(), + encryption_level: EncryptionLevel::None, + server_random: None, + server_cert: Vec::new(), + }); +pub static SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS: LazyLock = + LazyLock::new(|| ServerSecurityData { + encryption_method: EncryptionMethod::BIT_128, + encryption_level: EncryptionLevel::ClientCompatible, + server_random: Some(SERVER_RANDOM_BUFFER), + server_cert: SERVER_CERT_BUFFER.to_vec(), + }); +pub static SERVER_SECURITY_DATA_WITH_MISMATCH_OF_REQUIRED_AND_OPTIONAL_FIELDS: LazyLock = + LazyLock::new(|| ServerSecurityData { + encryption_method: EncryptionMethod::empty(), + encryption_level: EncryptionLevel::None, + server_random: Some(SERVER_RANDOM_BUFFER), + server_cert: SERVER_CERT_BUFFER.to_vec(), + }); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_BUFFER: [u8; 232] = concat_arrays!( + SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_PREFIX_BUFFER, + (SERVER_RANDOM_BUFFER.len() as u32).to_le_bytes(), + (SERVER_CERT_BUFFER.len() as u32).to_le_bytes(), + SERVER_RANDOM_BUFFER, + SERVER_CERT_BUFFER +); + +#[expect(clippy::as_conversions, reason = "must be const casts")] +pub const SERVER_SECURITY_DATA_WITH_INVALID_SERVER_RANDOM_BUFFER: [u8; 233] = concat_arrays!( + SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_PREFIX_BUFFER, + (SERVER_RANDOM_BUFFER.len() as u32 + 1).to_le_bytes(), + (SERVER_CERT_BUFFER.len() as u32).to_le_bytes(), + SERVER_RANDOM_BUFFER, + [0], + SERVER_CERT_BUFFER +); diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/channel_processing/minimized-from-46dc7754493e2231055dddc1c56c88b4b38e7a6e b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/channel_processing/minimized-from-46dc7754493e2231055dddc1c56c88b4b38e7a6e new file mode 100644 index 00000000..0db07781 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/channel_processing/minimized-from-46dc7754493e2231055dddc1c56c88b4b38e7a6e differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/cliprdr_format/crash-0e7b5df5737a7ffc553c46c2425609f543b89498 b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/cliprdr_format/crash-0e7b5df5737a7ffc553c46c2425609f543b89498 new file mode 100644 index 00000000..1b939403 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/cliprdr_format/crash-0e7b5df5737a7ffc553c46c2425609f543b89498 differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/cliprdr_format/minimized-from-a587e617baf5354b27cf9e6cbe92dc5d1b5cab7b b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/cliprdr_format/minimized-from-a587e617baf5354b27cf9e6cbe92dc5d1b5cab7b new file mode 100644 index 00000000..901b5076 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/cliprdr_format/minimized-from-a587e617baf5354b27cf9e6cbe92dc5d1b5cab7b differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/cliprdr_format/minimized-from-ae5d7850120bbeb84e519a5a0c29c2c93adddb80 b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/cliprdr_format/minimized-from-ae5d7850120bbeb84e519a5a0c29c2c93adddb80 new file mode 100644 index 00000000..ca7a3ce2 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/cliprdr_format/minimized-from-ae5d7850120bbeb84e519a5a0c29c2c93adddb80 differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-384d5ec1d99a93a3a53d206c9f808490d9b33d48 b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-384d5ec1d99a93a3a53d206c9f808490d9b33d48 new file mode 100644 index 00000000..f188400c Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-384d5ec1d99a93a3a53d206c9f808490d9b33d48 differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-3cf2ef026fc908048c16c4e816178b5b3002f7b1 b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-3cf2ef026fc908048c16c4e816178b5b3002f7b1 new file mode 100644 index 00000000..e2f912dc Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-3cf2ef026fc908048c16c4e816178b5b3002f7b1 differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-43677392fa06dda5f014e42dd06c9ae05cd19469 b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-43677392fa06dda5f014e42dd06c9ae05cd19469 new file mode 100644 index 00000000..51bdd187 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-43677392fa06dda5f014e42dd06c9ae05cd19469 differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-4485719597c99b83b2b761e497d03bf062b2461b b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-4485719597c99b83b2b761e497d03bf062b2461b new file mode 100644 index 00000000..efde99b6 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-4485719597c99b83b2b761e497d03bf062b2461b differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-638bb782419774c536a3455183fdbf172e5b830b b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-638bb782419774c536a3455183fdbf172e5b830b new file mode 100644 index 00000000..0427aa71 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-638bb782419774c536a3455183fdbf172e5b830b differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-65a50aad850d86155cae8c4c28f2937812cce53e b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-65a50aad850d86155cae8c4c28f2937812cce53e new file mode 100644 index 00000000..3b086690 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-65a50aad850d86155cae8c4c28f2937812cce53e differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-661a3fc68cec8096fb13ac76f3b50b85ad4c7ded b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-661a3fc68cec8096fb13ac76f3b50b85ad4c7ded new file mode 100644 index 00000000..9255d064 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-661a3fc68cec8096fb13ac76f3b50b85ad4c7ded differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-801238c30741729c04951985d7ea1238e930bf80 b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-801238c30741729c04951985d7ea1238e930bf80 new file mode 100644 index 00000000..2b9e6c4b Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-801238c30741729c04951985d7ea1238e930bf80 differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-93696ce8c9eec6824d9a3f99f960799d47b5d173 b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-93696ce8c9eec6824d9a3f99f960799d47b5d173 new file mode 100644 index 00000000..6b2901e3 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-93696ce8c9eec6824d9a3f99f960799d47b5d173 differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-ce45f9ebf4ab1a51fcd0175952b22ab0fbcdc392 b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-ce45f9ebf4ab1a51fcd0175952b22ab0fbcdc392 new file mode 100644 index 00000000..cdef4b17 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-ce45f9ebf4ab1a51fcd0175952b22ab0fbcdc392 differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-d58836860f9f3b70b3099cfbbed09f763d05eb4d b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-d58836860f9f3b70b3099cfbbed09f763d05eb4d new file mode 100644 index 00000000..3995cec8 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-d58836860f9f3b70b3099cfbbed09f763d05eb4d differ diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-f8dc2df3f2aba63122b8912dff23e13210b408e9 b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-f8dc2df3f2aba63122b8912dff23e13210b408e9 new file mode 100644 index 00000000..2ccc7f4a Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_decode/crash-f8dc2df3f2aba63122b8912dff23e13210b408e9 differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/cf_dib.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/cf_dib.pdu new file mode 100644 index 00000000..2fd8246b Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/cf_dib.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/cf_dibv5.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/cf_dibv5.pdu new file mode 100644 index 00000000..58bb0306 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/cf_dibv5.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/cf_html.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/cf_html.pdu new file mode 100644 index 00000000..8500deb0 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/cf_html.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/client_temp_dir.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/client_temp_dir.pdu new file mode 100644 index 00000000..d41f1890 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/client_temp_dir.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/file_list.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/file_list.pdu new file mode 100644 index 00000000..603e0864 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/file_list.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list.pdu new file mode 100644 index 00000000..6537176b Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list_2.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list_2.pdu new file mode 100644 index 00000000..c25366f9 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list_2.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/metafile.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/metafile.pdu new file mode 100644 index 00000000..947167ec Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/metafile.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/palette.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/palette.pdu new file mode 100644 index 00000000..d46cb644 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/palette.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/pointer/cached_pointer.bin b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/cached_pointer.bin new file mode 100644 index 00000000..09f370e3 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/cached_pointer.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_16bpp.png b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_16bpp.png new file mode 100644 index 00000000..213f5392 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_16bpp.png differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_1bpp.png b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_1bpp.png new file mode 100644 index 00000000..5c10ab92 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_1bpp.png differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_24bpp.bin b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_24bpp.bin new file mode 100644 index 00000000..b394e6d0 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_24bpp.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_24bpp.png b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_24bpp.png new file mode 100644 index 00000000..7d1304e0 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/color_pointer_24bpp.png differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/pointer/large_pointer_32bpp.bin b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/large_pointer_32bpp.bin new file mode 100644 index 00000000..9976ae0c Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/large_pointer_32bpp.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/pointer/large_pointer_32bpp.png b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/large_pointer_32bpp.png new file mode 100644 index 00000000..c0fd943a Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/large_pointer_32bpp.png differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/pointer/new_pointer_32bpp.bin b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/new_pointer_32bpp.bin new file mode 100644 index 00000000..4842eff8 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/new_pointer_32bpp.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/pointer/new_pointer_32bpp.png b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/new_pointer_32bpp.png new file mode 100644 index 00000000..27d30d8c Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/pointer/new_pointer_32bpp.png differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-27019fd9f222cebce9dfebcddb12bfa0-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-27019fd9f222cebce9dfebcddb12bfa0-compressed.bin new file mode 100644 index 00000000..cb7b744d Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-27019fd9f222cebce9dfebcddb12bfa0-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-27019fd9f222cebce9dfebcddb12bfa0-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-27019fd9f222cebce9dfebcddb12bfa0-decompressed.bin new file mode 100644 index 00000000..341aa763 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-27019fd9f222cebce9dfebcddb12bfa0-decompressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-284f668a9366a95e45f15b6bf634a633-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-284f668a9366a95e45f15b6bf634a633-compressed.bin new file mode 100644 index 00000000..0da995f0 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-284f668a9366a95e45f15b6bf634a633-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-284f668a9366a95e45f15b6bf634a633-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-284f668a9366a95e45f15b6bf634a633-decompressed.bin new file mode 100644 index 00000000..cf50e393 --- /dev/null +++ b/crates/ironrdp-testsuite-core/test_data/rle/tile-284f668a9366a95e45f15b6bf634a633-decompressed.bin @@ -0,0 +1 @@ +////0000////0000000000000000P0000000000PPP000000000QQPPP000000000000QQQQQPP0000000000qqqQQQQPPPP00000000qqqQQQQQQQQQ11000000rqqQQQQQQQQQQ1000000rrrrrqqqQQQQQQQ10000000000//rrrrrqQQQQQQQQQP000000000/rrrrrqqQQQQQQQQP000000000rrrrqQQQQQQQQ00PP00000rrrrqqQQQQQQQQQP000000rrrrqqQQQQQQQQP00000rrqQQQQQQQQQPP00000rrrqqqqQQQQQQPP000000rrrrqqqqqqQQQPPP00000000rqqqqqqQQQQP000000000rrqqqqqQQQQQ000000000rrrrrqQQQQ1000000000 \ No newline at end of file diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-28c08e75c82ab598c5ab85d1bfc00253-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-28c08e75c82ab598c5ab85d1bfc00253-compressed.bin new file mode 100644 index 00000000..e8f9beac Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-28c08e75c82ab598c5ab85d1bfc00253-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-28c08e75c82ab598c5ab85d1bfc00253-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-28c08e75c82ab598c5ab85d1bfc00253-decompressed.bin new file mode 100644 index 00000000..2d6d104a Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-28c08e75c82ab598c5ab85d1bfc00253-decompressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-2de3f3262a5eeecc3152552c178b782a-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-2de3f3262a5eeecc3152552c178b782a-compressed.bin new file mode 100644 index 00000000..27e08d67 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-2de3f3262a5eeecc3152552c178b782a-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-2de3f3262a5eeecc3152552c178b782a-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-2de3f3262a5eeecc3152552c178b782a-decompressed.bin new file mode 100644 index 00000000..e7663d71 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-2de3f3262a5eeecc3152552c178b782a-decompressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-3fc8124af9be2fe88b445db60c36eddc-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-3fc8124af9be2fe88b445db60c36eddc-compressed.bin new file mode 100644 index 00000000..bac13ba6 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-3fc8124af9be2fe88b445db60c36eddc-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-3fc8124af9be2fe88b445db60c36eddc-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-3fc8124af9be2fe88b445db60c36eddc-decompressed.bin new file mode 100644 index 00000000..fb10adcc --- /dev/null +++ b/crates/ironrdp-testsuite-core/test_data/rle/tile-3fc8124af9be2fe88b445db60c36eddc-decompressed.bin @@ -0,0 +1 @@ +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk \ No newline at end of file diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-4d75aa6a18c435c6230ba739b802a861-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-4d75aa6a18c435c6230ba739b802a861-compressed.bin new file mode 100644 index 00000000..eb5b467d Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-4d75aa6a18c435c6230ba739b802a861-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-4d75aa6a18c435c6230ba739b802a861-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-4d75aa6a18c435c6230ba739b802a861-decompressed.bin new file mode 100644 index 00000000..7e2fa2ee Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-4d75aa6a18c435c6230ba739b802a861-decompressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-8b8ccc77526730d0cd8989901cc031ec-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-8b8ccc77526730d0cd8989901cc031ec-compressed.bin new file mode 100644 index 00000000..aee0c0df Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-8b8ccc77526730d0cd8989901cc031ec-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-8b8ccc77526730d0cd8989901cc031ec-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-8b8ccc77526730d0cd8989901cc031ec-decompressed.bin new file mode 100644 index 00000000..a6024ad8 --- /dev/null +++ b/crates/ironrdp-testsuite-core/test_data/rle/tile-8b8ccc77526730d0cd8989901cc031ec-decompressed.bin @@ -0,0 +1 @@ +SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKSSSSSKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKKKKKKKKKKKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKKKKKKKKKKKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKKKKKKKKKKKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKKKKKKKKKKKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKKKKKKKoKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKKKKKKKKoKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKKKKKKKKKoKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKSSKKoKKKKKKKKKKKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKSKKKoKKKKKKKKKKKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKoKKKKKKKoKoKKKKKoKoKoKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKoKoKoKoKoKKKKKoKoKoKKKKoKoKoKoKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKoKoKoKoKoKKSSKoKoKoKKKKoKoKoKoKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSoSSSoSoSSSoKoKKKKKKKKoKoKKKoKoKoKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSoSSSoSoSSSoKoKKKKKKKKoKoKKoKoKoKoKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSoSSSoSoSSSoKoKKKKKKKKoKoKKoKoKoKoKKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSoSoSoSoSSSoSoSSSoKoKKoKoKoKoKKKoKoKoKoKoKoKoKoKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSoSoSSSSSoSoSoSoKoKoSoSoKoKoSoSoKoKKoKoKoKoKKKoKoKoKoKoKoKoKoKSSSSSSSSSSSSSSSSSSSSSSSSSSSSoSoSoSoSSSSSoSoSoSoKoKoSoSoKoKoSoSoKKoKoKoKoKoKoKKoKoKoKoKoKoKoKoKSSSSSSSSSSSSSSSSSSSSSSSSSKKKKoKoKoKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKSSSSSSSSSSSSSSSSSSSSSSSSSSSKKKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKKKoKoKoKoKoKoKoKoKoKoKoKKKKKKKKKSSSKKKoKoKKKoKoKoKoKKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKKKKKoKoKoKoKoKoKoKoKoKoKKKKKKKKKSSKKKKoKoKKKoKoKoKoKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoKoSoSoSoSoSoSoSoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoKoSoSoSoSoSoSoKoKoKoKoSoSoSoSoSoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoKoSoSoSoSoSoSoKoKoKoKoKoSoSoSoSoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoSoKoSoSoSoSoSoSoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoSoSoSoSoSoSoSoKoSoSoSoSoSoSoSoSoSoSoKoKoKoKoSoSoSoSoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoSoSoSoSoSoSoKoKoSoSoSoSoSoSoSoSoKoKoKoKoKoKoSoSoSoSoSoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKKoKoKoKoKoKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKpKpKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKoKoKoKOKOKOKOKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKoKoKoKOKOKOKOKOKoKoKoKoKoKoKoKoKoKoKoKpKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKoKoKOKOKOKOKOKOKOKOKoKoKoKoKoKoKoKoKoKOKOKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOKOKOKOKoKOKOKOKOKOKOKoKOKOKOKOKOKOKOKOKoKoKoKoKoKoKoKoKOKOKOKOKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOKOKOKoKoKoKoKoKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKoKoKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKoKoKoKoKoKoKoKoKOKOKOKOKOKOCOCOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKoKOKOKOCOCOKOKoKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOKOKOKOCOCOCOCOCOCOCOCOCOCOCOKOCOCOCOCOCOCOCOCOKOKOKOCOCOCOCOCOCOCOCOCOKOKOKOKOKOCOCOKOKOKOCOCOCOCOCOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOKOCOCOCOCOCOCOCOCOKOKOKOCOCOCOCOCOCOCOCOCOCOCPKPKOKOKOKOKPKPKOCOCOKOKOKOKPKPKOCOCOCOCOCOCOCOCOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOKOKOKOKOKOKOKOKOCOCOCOCOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCPCOCPCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC \ No newline at end of file diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-94bb5b131eb3bc110905dfcb0f60da79-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-94bb5b131eb3bc110905dfcb0f60da79-compressed.bin new file mode 100644 index 00000000..db925470 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-94bb5b131eb3bc110905dfcb0f60da79-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-94bb5b131eb3bc110905dfcb0f60da79-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-94bb5b131eb3bc110905dfcb0f60da79-decompressed.bin new file mode 100644 index 00000000..f6c26d8d --- /dev/null +++ b/crates/ironrdp-testsuite-core/test_data/rle/tile-94bb5b131eb3bc110905dfcb0f60da79-decompressed.bin @@ -0,0 +1 @@ +SSSSKKKKKKKKKKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCoCOCOCKKKSKKKKKKKKKKKKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCoCoCOCOCoKoKoKKKKKKKKKKKKKKKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCoCOCOCOCoKoKoKKKKKKKKKKKoKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCOCOCOCOCoKKKKKKKKKKKKKoKoKoKoKoKoKoKoKoKoKoKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCoCoCOCOCOCOCOCKKKKKKKKoKoKKKKoKoKoKoKoKoKoKoKoKKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCoCOCOCOCOCOCOCoKoKKKKKKKoKoKoKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOCOCOCOCOCOCOCOCoKoKoKoKKKoKoKoKoKoKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOCOCOCOCOCOCOCOCKKKoKKKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCOCOCoCoCoCOCOCOCKoKoKoKKKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCoCOCOCOCOCOCOCOCOCoKoKoKoKKKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCOCOCOCOCOCOCOCOCOCoKoKoKoKKKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCoCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCoCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoCoCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKoKoKoKoKoKoKOKOKoCOCOCoCoCoCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKoCOCoCoCoCoCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOCOCoCoCoCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOCOCoCoCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKoKoKoKoKoKoKoKoKoKoKoKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKoKoKOCOCoKoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKoKoKoKoKoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKoKOCoKoKoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKoKoKoKoKOKOCOCoKoKoKOCOCOCOCoCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKoKoKoKOKOCOCoKoKoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOKOCOCoKoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKoKoKOKOKOKOKOCoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKoKoKoKoKoKoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKoKoKoKoKoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOCoKoKoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOCoKoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOKOKOCOCoKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/COCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/COCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/COCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKoKoKoKoKoKOKOKOKOKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/COCoKoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/COCOC/C/C/C/C/C/CoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/COC/C/C/C/C/C/C/CoKoKoKoKoKoKoKoKoKoKoKoKoKoKOKOKoKoKOKOKOKOKOKOKOKOCOCOCOCOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/CoKoKoKoKoKoKoKoKOKOKoKoKoKOKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/CoKoKoKoKoKoKoKoKOKOKOKoKOKOKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/CoKoKoKoKoKoKoKoKOKOKOKoKoKOKOKOKOKOKOKOKOCOCOCOCOKOCOCOCOCOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/CoKoKOKOKOKOKoKoKOKOKOKoKoKOKOKOKOKOKOKOKOKOCOCOCOKOCOCOCOCOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/CoKoKOKOKOKOKoKoKOKOKoKoKoKoKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/COKOKoKoKoKOKOKOKoKoKoKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/COKOKoKoKOKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/CoKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/CoKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/COKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/COKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/COCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/COKOKOKOKOKOCOCOCOKOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/COCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/COCOCOKOKOKOCOCOCOKOKOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/COCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/COCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/COCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/COCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/COCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/;/;/;OCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/;/;/;/;OCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/;/;/C/C/C/;/;/;/;/;OCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/;/;/;/;/;/;/;OCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/COCOCOCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/;/;/;/;/;/;/;/;OCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOC/C/C/COCOC/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/C/;/;/;/;/;/;/;/;/;/; \ No newline at end of file diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-9b06660a1da806d2d48ce3f46b45d571-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-9b06660a1da806d2d48ce3f46b45d571-compressed.bin new file mode 100644 index 00000000..369f3c31 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-9b06660a1da806d2d48ce3f46b45d571-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-9b06660a1da806d2d48ce3f46b45d571-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-9b06660a1da806d2d48ce3f46b45d571-decompressed.bin new file mode 100644 index 00000000..991081c2 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-9b06660a1da806d2d48ce3f46b45d571-decompressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-a412fbe2b435ac627ce39048aa3d3fb3-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-a412fbe2b435ac627ce39048aa3d3fb3-compressed.bin new file mode 100644 index 00000000..c744b1f4 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-a412fbe2b435ac627ce39048aa3d3fb3-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-a412fbe2b435ac627ce39048aa3d3fb3-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-a412fbe2b435ac627ce39048aa3d3fb3-decompressed.bin new file mode 100644 index 00000000..c1adc696 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-a412fbe2b435ac627ce39048aa3d3fb3-decompressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-aa326e7a536cc8a0420c44bdf4ef8d97-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-aa326e7a536cc8a0420c44bdf4ef8d97-compressed.bin new file mode 100644 index 00000000..fc564f85 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-aa326e7a536cc8a0420c44bdf4ef8d97-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-aa326e7a536cc8a0420c44bdf4ef8d97-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-aa326e7a536cc8a0420c44bdf4ef8d97-decompressed.bin new file mode 100644 index 00000000..e5567c04 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-aa326e7a536cc8a0420c44bdf4ef8d97-decompressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-fbcefc9af4db651aefd91bcabc8ea9fc-compressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-fbcefc9af4db651aefd91bcabc8ea9fc-compressed.bin new file mode 100644 index 00000000..5cba850a Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-fbcefc9af4db651aefd91bcabc8ea9fc-compressed.bin differ diff --git a/crates/ironrdp-testsuite-core/test_data/rle/tile-fbcefc9af4db651aefd91bcabc8ea9fc-decompressed.bin b/crates/ironrdp-testsuite-core/test_data/rle/tile-fbcefc9af4db651aefd91bcabc8ea9fc-decompressed.bin new file mode 100644 index 00000000..d3d5b4a2 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/rle/tile-fbcefc9af4db651aefd91bcabc8ea9fc-decompressed.bin differ diff --git a/crates/ironrdp-testsuite-core/tests/clipboard/format.rs b/crates/ironrdp-testsuite-core/tests/clipboard/format.rs new file mode 100644 index 00000000..ce2e553f --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/clipboard/format.rs @@ -0,0 +1,54 @@ +use ironrdp_cliprdr_format::bitmap::{dib_to_png, dibv5_to_png, png_to_cf_dib, png_to_cf_dibv5}; +use ironrdp_cliprdr_format::html::{cf_html_to_plain_html, plain_html_to_cf_html}; + +#[test] +fn dib_to_png_conversion_1() { + let input = include_bytes!("../../test_data/pdu/clipboard/cf_dib.pdu"); + let png = dib_to_png(input).unwrap(); + let converted = png_to_cf_dib(&png).unwrap(); + assert_eq!(converted, input); +} + +#[test] +fn dibv5_to_png_conversion_1() { + let input = include_bytes!("../../test_data/pdu/clipboard/cf_dibv5.pdu"); + let png = dibv5_to_png(input).unwrap(); + let converted = png_to_cf_dibv5(&png).unwrap(); + assert_eq!(converted, input); +} + +#[test] +fn html_failure() { + // Empty + assert!(cf_html_to_plain_html(&[]).is_err()); + // Garbage + assert!(cf_html_to_plain_html(&[0x00, 0x01, 0x02, 0x03]).is_err()); + // No headers + assert!(cf_html_to_plain_html(b"hello world").is_err()); + // Headers with fragment size not found + assert!(cf_html_to_plain_html(b"Version:0.9\r\nnopers").is_err()); + // Out of bounds headers + assert!(cf_html_to_plain_html(b"StartFragment:999\r\nEndFragment:9999\r\nnopers").is_err()); +} + +#[test] +fn test_cf_html_to_text() { + let input = include_bytes!("../../test_data/pdu/clipboard/cf_html.pdu"); + let actual = cf_html_to_plain_html(input).unwrap(); + + // Validate that the output is valid HTML + assert!(actual.starts_with("Remote Desktop Protocol")); + assert!(actual.ends_with("")); + + // Validate roundtrip + let cf_html = plain_html_to_cf_html(actual); + let roundtrip_html_text = cf_html_to_plain_html(cf_html.as_bytes()).unwrap(); + assert_eq!(actual, roundtrip_html_text); + + // Add some padding (CF_HTML is not null-terminated, we need to work with data which is + // potentially padded with arbitrary fill bytes). + let mut cf_html = cf_html.into_bytes(); + cf_html.extend_from_slice(&[0xFF; 10]); + let roundtrip_html_text = cf_html_to_plain_html(&cf_html).unwrap(); + assert_eq!(actual, roundtrip_html_text); +} diff --git a/crates/ironrdp-testsuite-core/tests/clipboard/mod.rs b/crates/ironrdp-testsuite-core/tests/clipboard/mod.rs new file mode 100644 index 00000000..dc0641f4 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/clipboard/mod.rs @@ -0,0 +1,445 @@ +mod format; + +use expect_test::expect; +use ironrdp_cliprdr::pdu::{ + Capabilities, CapabilitySet, ClipboardFormat, ClipboardFormatId, ClipboardFormatName, + ClipboardGeneralCapabilityFlags, ClipboardPdu, ClipboardProtocolVersion, FileContentsFlags, FileContentsRequest, + FileContentsResponse, FormatDataRequest, FormatDataResponse, FormatList, FormatListResponse, GeneralCapabilitySet, + LockDataId, PackedMetafileMappingMode, +}; +use ironrdp_testsuite_core::encode_decode_test; + +// Test blobs from [MS-RDPECLIP] +encode_decode_test! { + capabilities: + ClipboardPdu::Capabilities( + Capabilities { + capabilities: vec![ + CapabilitySet::General( + GeneralCapabilitySet { + version: ClipboardProtocolVersion::V2, + general_flags: ClipboardGeneralCapabilityFlags::USE_LONG_FORMAT_NAMES + | ClipboardGeneralCapabilityFlags::STREAM_FILECLIP_ENABLED + | ClipboardGeneralCapabilityFlags::FILECLIP_NO_FILE_PATHS, + } + ) + ] + } + ), + [ + 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0c, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + ]; + + monitor_ready: + ClipboardPdu::MonitorReady, + [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + format_list_response: + ClipboardPdu::FormatListResponse(FormatListResponse::Ok), + [ + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + lock: + ClipboardPdu::LockData(LockDataId(8)), + [ + 0x0a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, + ]; + + unlock: + ClipboardPdu::UnlockData(LockDataId(8)), + [ + 0x0b, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, + ]; + + format_data_request: + ClipboardPdu::FormatDataRequest(FormatDataRequest { + format: ClipboardFormatId::new(0x0d), + }), + [ + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, + ]; + + format_data_response: + ClipboardPdu::FormatDataResponse( + FormatDataResponse::new_data(b"h\0e\0l\0l\0o\0 \0w\0o\0r\0l\0d\0\0\0".as_slice()), + ), + [ + 0x05, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x68, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, + 0x6f, 0x00, 0x20, 0x00, 0x77, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x6c, 0x00, 0x64, 0x00, 0x00, 0x00, + ]; + + file_contents_request_size: + ClipboardPdu::FileContentsRequest(FileContentsRequest { + stream_id: 2, + index: 1, + flags: FileContentsFlags::SIZE, + position: 0, + requested_size: 8, + data_id: None, + }), + [ + 0x08, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + ]; + + file_contents_request_data: + ClipboardPdu::FileContentsRequest(FileContentsRequest { + stream_id: 2, + index: 1, + flags: FileContentsFlags::DATA, + position: 0, + requested_size: 65536, + data_id: None, + }), + [ + 0x08, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + ]; + + file_contents_response_size: + ClipboardPdu::FileContentsResponse(FileContentsResponse::new_size_response(2, 44)), + [ + 0x09, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + file_contents_response_data: + ClipboardPdu::FileContentsResponse(FileContentsResponse::new_data_response( + 2, + b"The quick brown fox jumps over the lazy dog.".as_slice() + )), + [ + 0x09, 0x00, 0x01, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x54, 0x68, 0x65, 0x20, + 0x71, 0x75, 0x69, 0x63, 0x6b, 0x20, 0x62, 0x72, + 0x6f, 0x77, 0x6e, 0x20, 0x66, 0x6f, 0x78, 0x20, + 0x6a, 0x75, 0x6d, 0x70, 0x73, 0x20, 0x6f, 0x76, + 0x65, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6c, + 0x61, 0x7a, 0x79, 0x20, 0x64, 0x6f, 0x67, 0x2e, + ]; +} + +#[test] +fn client_temp_dir_encode_decode_ms_1() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../../test_data/pdu/clipboard/client_temp_dir.pdu"); + + let decoded_pdu: ClipboardPdu<'_> = ironrdp_core::decode(input).unwrap(); + + if let ClipboardPdu::TemporaryDirectory(client_temp_dir) = &decoded_pdu { + let path = client_temp_dir.temporary_directory_path().unwrap(); + expect![[r"C:\DOCUME~1\ELTONS~1.NTD\LOCALS~1\Temp\cdepotslhrdp_1\_TSABD.tmp"]].assert_eq(&path); + } else { + panic!("Expected ClientTemporaryDirectory"); + } + + let encoded = ironrdp_core::encode_vec(&decoded_pdu).unwrap(); + + assert_eq!(&encoded, input); +} + +#[test] +fn format_list_ms_1() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../../test_data/pdu/clipboard/format_list.pdu"); + + let decoded_pdu: ClipboardPdu<'_> = ironrdp_core::decode(input).unwrap(); + + if let ClipboardPdu::FormatList(format_list) = &decoded_pdu { + let formats = format_list.get_formats(true).unwrap(); + + expect![[r#" + [ + ClipboardFormat { + id: ClipboardFormatId( + 49156, + ), + name: Some( + ClipboardFormatName( + "Native", + ), + ), + }, + ClipboardFormat { + id: ClipboardFormatId( + 3, + ), + name: None, + }, + ClipboardFormat { + id: ClipboardFormatId( + 8, + ), + name: None, + }, + ClipboardFormat { + id: ClipboardFormatId( + 17, + ), + name: None, + }, + ] + "#]] + .assert_debug_eq(&formats); + + formats + } else { + panic!("Expected FormatList"); + }; + + let encoded = ironrdp_core::encode_vec(&decoded_pdu).unwrap(); + + assert_eq!(&encoded, input); +} + +#[test] +fn format_list_ms_2() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../../test_data/pdu/clipboard/format_list_2.pdu"); + + let decoded_pdu: ClipboardPdu<'_> = ironrdp_core::decode(input).unwrap(); + + if let ClipboardPdu::FormatList(format_list) = &decoded_pdu { + let formats = format_list.get_formats(true).unwrap(); + + expect![[r#" + [ + ClipboardFormat { + id: ClipboardFormatId( + 49290, + ), + name: Some( + ClipboardFormatName( + "Rich Text Format", + ), + ), + }, + ClipboardFormat { + id: ClipboardFormatId( + 49477, + ), + name: Some( + ClipboardFormatName( + "Rich Text Format Without Objects", + ), + ), + }, + ClipboardFormat { + id: ClipboardFormatId( + 49475, + ), + name: Some( + ClipboardFormatName( + "RTF As Text", + ), + ), + }, + ClipboardFormat { + id: ClipboardFormatId( + 1, + ), + name: None, + }, + ClipboardFormat { + id: ClipboardFormatId( + 13, + ), + name: None, + }, + ClipboardFormat { + id: ClipboardFormatId( + 49156, + ), + name: Some( + ClipboardFormatName( + "Native", + ), + ), + }, + ClipboardFormat { + id: ClipboardFormatId( + 49166, + ), + name: Some( + ClipboardFormatName( + "Object Descriptor", + ), + ), + }, + ClipboardFormat { + id: ClipboardFormatId( + 3, + ), + name: None, + }, + ClipboardFormat { + id: ClipboardFormatId( + 16, + ), + name: None, + }, + ClipboardFormat { + id: ClipboardFormatId( + 7, + ), + name: None, + }, + ] + "#]] + .assert_debug_eq(&formats); + + formats + } else { + panic!("Expected FormatList"); + }; + + let encoded = ironrdp_core::encode_vec(&decoded_pdu).unwrap(); + + assert_eq!(&encoded, input); +} + +fn fake_format_list(use_ascii: bool, use_long_format: bool) -> FormatList<'static> { + let formats = vec![ + ClipboardFormat::new(ClipboardFormatId::new(42)).with_name(ClipboardFormatName::new("Hello")), + ClipboardFormat::new(ClipboardFormatId::new(24)), + ClipboardFormat::new(ClipboardFormatId::new(11)).with_name(ClipboardFormatName::new("World")), + ]; + + let list = if use_ascii { + FormatList::new_ascii(&formats, use_long_format).unwrap() + } else { + FormatList::new_unicode(&formats, use_long_format).unwrap() + }; + + list +} + +#[test] +fn format_list_all_encodings() { + // ASCII, short format names + fake_format_list(true, false); + // ASCII, long format names + fake_format_list(true, true); + // Unicode, short format names + fake_format_list(false, false); + // Unicode, long format names + fake_format_list(false, true); +} + +#[test] +fn metafile_pdu_ms() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../../test_data/pdu/clipboard/metafile.pdu"); + + let decoded_pdu: ClipboardPdu<'_> = ironrdp_core::decode(input).unwrap(); + + if let ClipboardPdu::FormatDataResponse(response) = &decoded_pdu { + let metafile = response.to_metafile().unwrap(); + + assert_eq!(metafile.mapping_mode, PackedMetafileMappingMode::ANISOTROPIC); + assert_eq!(metafile.x_ext, 556); + assert_eq!(metafile.y_ext, 423); + + // Just check some known arbitrary byte in raw metafile data + assert_eq!(metafile.data[metafile.data.len() - 6], 0x03); + } else { + panic!("Expected FormatDataResponse"); + }; + + let encoded = ironrdp_core::encode_vec(&decoded_pdu).unwrap(); + + assert_eq!(&encoded, input); +} + +#[test] +fn palette_pdu_ms() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../../test_data/pdu/clipboard/palette.pdu"); + + let decoded_pdu: ClipboardPdu<'_> = ironrdp_core::decode(input).unwrap(); + + if let ClipboardPdu::FormatDataResponse(response) = &decoded_pdu { + let palette = response.to_palette().unwrap(); + + assert_eq!(palette.entries.len(), 216); + + // Chack known palette color + assert_eq!(palette.entries[53].red, 0xff); + assert_eq!(palette.entries[53].green, 0x66); + assert_eq!(palette.entries[53].blue, 0x33); + assert_eq!(palette.entries[53].extra, 0x00); + } else { + panic!("Expected FormatDataResponse"); + }; + + let encoded = ironrdp_core::encode_vec(&decoded_pdu).unwrap(); + + assert_eq!(&encoded, input); +} + +#[test] +fn file_list_pdu_ms() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../../test_data/pdu/clipboard/file_list.pdu"); + + let decoded_pdu: ClipboardPdu<'_> = ironrdp_core::decode(input).unwrap(); + + if let ClipboardPdu::FormatDataResponse(response) = &decoded_pdu { + let file_list = response.to_file_list().unwrap(); + + expect![[r#" + [ + FileDescriptor { + attributes: Some( + ClipboardFileAttributes( + ARCHIVE, + ), + ), + last_write_time: Some( + 129010042240261384, + ), + file_size: Some( + 44, + ), + name: "File1.txt", + }, + FileDescriptor { + attributes: Some( + ClipboardFileAttributes( + ARCHIVE, + ), + ), + last_write_time: Some( + 129010042240261384, + ), + file_size: Some( + 10, + ), + name: "File2.txt", + }, + ] + "#]] + .assert_debug_eq(&file_list.files) + } else { + panic!("Expected FormatDataResponse"); + }; + + let encoded = ironrdp_core::encode_vec(&decoded_pdu).unwrap(); + + assert_eq!(&encoded, input); +} diff --git a/crates/ironrdp-testsuite-core/tests/displaycontrol/mod.rs b/crates/ironrdp-testsuite-core/tests/displaycontrol/mod.rs new file mode 100644 index 00000000..5a31df70 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/displaycontrol/mod.rs @@ -0,0 +1,127 @@ +use ironrdp_core::decode; +use ironrdp_displaycontrol::pdu; +use ironrdp_testsuite_core::encode_decode_test; + +encode_decode_test! { + capabilities: pdu::DisplayControlPdu::Caps(pdu::DisplayControlCapabilities::new( + 3, 1920, 1080 + ).unwrap()), + [ + // Header + 0x05, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, + // Payload + 0x03, 0x00, 0x00, 0x00, + 0x80, 0x07, 0x00, 0x00, + 0x38, 0x04, 0x00, 0x00, + ]; + + layout: pdu::DisplayControlPdu::MonitorLayout(pdu::DisplayControlMonitorLayout::new( + &[ + pdu::MonitorLayoutEntry::new_primary(1920, 1080).unwrap() + .with_orientation(pdu::MonitorOrientation::LandscapeFlipped) + .with_physical_dimensions(1000, 500).unwrap() + .with_position(0, 0).unwrap() + .with_device_scale_factor(pdu::DeviceScaleFactor::Scale140Percent) + .with_desktop_scale_factor(150).unwrap(), + pdu::MonitorLayoutEntry::new_secondary(1024, 768).unwrap() + .with_orientation(pdu::MonitorOrientation::Portrait) + .with_physical_dimensions(500, 500).unwrap() + .with_position(-500, 0).unwrap() + .with_device_scale_factor(pdu::DeviceScaleFactor::Scale100Percent) + .with_desktop_scale_factor(100).unwrap() + ] + ).unwrap()), + [ + // Header + 0x02, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, + // Payload + 0x28, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, + + // Monitor 1 + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x80, 0x07, 0x00, 0x00, + 0x38, 0x04, 0x00, 0x00, + 0xE8, 0x03, 0x00, 0x00, + 0xF4, 0x01, 0x00, 0x00, + 0xB4, 0x00, 0x00, 0x00, + 0x96, 0x00, 0x00, 0x00, + 0x8C, 0x00, 0x00, 0x00, + + // Monitor 2 + 0x00, 0x00, 0x00, 0x00, + 0x0C, 0xFE, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, + 0xF4, 0x01, 0x00, 0x00, + 0xF4, 0x01, 0x00, 0x00, + 0x5A, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x00, + ]; +} + +#[test] +fn invalid_caps() { + pdu::DisplayControlCapabilities::new(2000, 100, 100).expect_err("more than 1024 monitors should not be allowed"); + + pdu::DisplayControlCapabilities::new(100, 32 * 1024, 100) + .expect_err("resolution more than 8k should not be a valid value"); +} + +#[test] +fn monitor_layout_entry_odd_dimensions_adjustment() { + let odd_value = 1023; + let entry = pdu::MonitorLayoutEntry::new_primary(odd_value, odd_value).expect("valid entry should be created"); + let (width, height) = entry.dimensions(); + assert_eq!(width, odd_value - 1); + assert_eq!(height, odd_value); +} + +#[test] +fn invalid_monitor_layout_entry() { + pdu::MonitorLayoutEntry::new_primary(32 * 1024, 32 * 1024) + .expect_err("resolution more than 8k should not be allowed"); + + pdu::MonitorLayoutEntry::new_primary(1024, 1024) + .unwrap() + .with_position(-1, 1) + .expect_err("primary monitor should always have (0, 0) position"); + + pdu::MonitorLayoutEntry::new_primary(1024, 1024) + .unwrap() + .with_position(-1, 1) + .expect_err("primary monitor should always have (0, 0) position"); + + pdu::MonitorLayoutEntry::new_primary(1024, 1024) + .unwrap() + .with_desktop_scale_factor(999) + .expect_err("invalid desktop factor should be rejected"); + + pdu::MonitorLayoutEntry::new_primary(1024, 1024) + .unwrap() + .with_physical_dimensions(1, 9999) + .expect_err("invalid physical dimensions should be rejected"); +} + +#[test] +fn only_non_optional_layout_fields_required_to_be_valid() { + let encoded = [ + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x38, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + ]; + + let decoded = decode::(&encoded).unwrap(); + + assert!(decoded.desktop_scale_factor().is_none()); + assert!(decoded.device_scale_factor().is_none()); + assert!(decoded.orientation().is_none()); + assert!(decoded.physical_dimensions().is_none()); + assert!(decoded.position().is_none()) +} diff --git a/crates/ironrdp-testsuite-core/tests/dvc/capabilities.rs b/crates/ironrdp-testsuite-core/tests/dvc/capabilities.rs new file mode 100644 index 00000000..9a398e8b --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/dvc/capabilities.rs @@ -0,0 +1,57 @@ +use super::*; + +const REQ_V1_ENCODED: [u8; 4] = [0x50, 0x00, 0x01, 0x00]; +const REQ_V2_ENCODED: [u8; 12] = [0x50, 0x00, 0x02, 0x00, 0x33, 0x33, 0x11, 0x11, 0x3d, 0x0a, 0xa7, 0x04]; +const RESP_V1_ENCODED: [u8; 4] = [0x50, 0x00, 0x01, 0x00]; + +static REQ_V1_DECODED_SERVER: OnceLock = OnceLock::new(); +static REQ_V2_DECODED_SERVER: OnceLock = OnceLock::new(); +static RESP_V1_DECODED_CLIENT: OnceLock = OnceLock::new(); + +fn req_v1_decoded_server() -> &'static DrdynvcServerPdu { + REQ_V1_DECODED_SERVER + .get_or_init(|| DrdynvcServerPdu::Capabilities(CapabilitiesRequestPdu::new(CapsVersion::V1, None))) +} + +fn req_v2_decoded_server() -> &'static DrdynvcServerPdu { + REQ_V2_DECODED_SERVER.get_or_init(|| { + DrdynvcServerPdu::Capabilities(CapabilitiesRequestPdu::new( + CapsVersion::V2, + Some([0x3333, 0x1111, 0x0a3d, 0x04a7]), + )) + }) +} + +fn resp_v1_decoded_client() -> &'static DrdynvcClientPdu { + RESP_V1_DECODED_CLIENT.get_or_init(|| DrdynvcClientPdu::Capabilities(CapabilitiesResponsePdu::new(CapsVersion::V1))) +} + +#[test] +fn decodes_request_v1() { + test_decodes(&REQ_V1_ENCODED, req_v1_decoded_server()); +} + +#[test] +fn encodes_request_v1() { + test_encodes(req_v1_decoded_server(), &REQ_V1_ENCODED); +} + +#[test] +fn decodes_request_v2() { + test_decodes(&REQ_V2_ENCODED, req_v2_decoded_server()); +} + +#[test] +fn encodes_request_v2() { + test_encodes(req_v2_decoded_server(), &REQ_V2_ENCODED); +} + +#[test] +fn decodes_response_v1() { + test_decodes(&RESP_V1_ENCODED, resp_v1_decoded_client()); +} + +#[test] +fn encodes_response_v1() { + test_encodes(resp_v1_decoded_client(), &RESP_V1_ENCODED); +} diff --git a/crates/ironrdp-testsuite-core/tests/dvc/close.rs b/crates/ironrdp-testsuite-core/tests/dvc/close.rs new file mode 100644 index 00000000..c7279de1 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/dvc/close.rs @@ -0,0 +1,27 @@ +use super::*; + +const CHANNEL_ID: u32 = 0x0303; +const ENCODED: [u8; 3] = [0x41, 0x03, 0x03]; + +static DECODED_CLIENT: OnceLock = OnceLock::new(); +static DECODED_SERVER: OnceLock = OnceLock::new(); + +fn decoded_client() -> &'static DrdynvcClientPdu { + DECODED_CLIENT.get_or_init(|| DrdynvcClientPdu::Close(ClosePdu::new(CHANNEL_ID).with_cb_id_type(FieldType::U16))) +} + +fn decoded_server() -> &'static DrdynvcServerPdu { + DECODED_SERVER.get_or_init(|| DrdynvcServerPdu::Close(ClosePdu::new(CHANNEL_ID).with_cb_id_type(FieldType::U16))) +} + +#[test] +fn decodes_close() { + test_decodes(&ENCODED, decoded_client()); + test_decodes(&ENCODED, decoded_server()); +} + +#[test] +fn encodes_close() { + test_encodes(decoded_client(), &ENCODED); + test_encodes(decoded_server(), &ENCODED); +} diff --git a/crates/ironrdp-testsuite-core/tests/dvc/create.rs b/crates/ironrdp-testsuite-core/tests/dvc/create.rs new file mode 100644 index 00000000..9740ffe0 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/dvc/create.rs @@ -0,0 +1,37 @@ +use super::*; + +const CHANNEL_ID: u32 = 0x0000_0003; +const REQ_ENCODED: [u8; 10] = [0x10, 0x03, 0x74, 0x65, 0x73, 0x74, 0x64, 0x76, 0x63, 0x00]; +const RESP_ENCODED: [u8; 6] = [0x10, 0x03, 0x00, 0x00, 0x00, 0x00]; + +static REQ_DECODED_SERVER: OnceLock = OnceLock::new(); +static RESP_DECODED_CLIENT: OnceLock = OnceLock::new(); + +fn req_decoded_server() -> &'static DrdynvcServerPdu { + REQ_DECODED_SERVER + .get_or_init(|| DrdynvcServerPdu::Create(CreateRequestPdu::new(CHANNEL_ID, String::from("testdvc")))) +} + +fn resp_decoded_client() -> &'static DrdynvcClientPdu { + RESP_DECODED_CLIENT.get_or_init(|| DrdynvcClientPdu::Create(CreateResponsePdu::new(CHANNEL_ID, CreationStatus::OK))) +} + +#[test] +fn decodes_create_request() { + test_decodes(&REQ_ENCODED, req_decoded_server()); +} + +#[test] +fn encodes_create_request() { + test_encodes(req_decoded_server(), &REQ_ENCODED); +} + +#[test] +fn decodes_create_response() { + test_decodes(&RESP_ENCODED, resp_decoded_client()); +} + +#[test] +fn encodes_create_response() { + test_encodes(resp_decoded_client(), &RESP_ENCODED); +} diff --git a/crates/ironrdp-testsuite-core/tests/dvc/data.rs b/crates/ironrdp-testsuite-core/tests/dvc/data.rs new file mode 100644 index 00000000..93d40462 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/dvc/data.rs @@ -0,0 +1,37 @@ +use super::*; + +const CHANNEL_ID: u32 = 0x03; +const PREFIX: [u8; 2] = [0x30, 0x03]; +const DATA: [u8; 12] = [0x71; 12]; + +static ENCODED: OnceLock> = OnceLock::new(); +static DECODED_CLIENT: OnceLock = OnceLock::new(); +static DECODED_SERVER: OnceLock = OnceLock::new(); + +fn encoded() -> &'static Vec { + ENCODED.get_or_init(|| { + let mut result = PREFIX.to_vec(); + result.extend(&DATA); + result + }) +} + +fn decoded_client() -> &'static DrdynvcClientPdu { + DECODED_CLIENT.get_or_init(|| DrdynvcClientPdu::Data(DrdynvcDataPdu::Data(DataPdu::new(CHANNEL_ID, DATA.to_vec())))) +} + +fn decoded_server() -> &'static DrdynvcServerPdu { + DECODED_SERVER.get_or_init(|| DrdynvcServerPdu::Data(DrdynvcDataPdu::Data(DataPdu::new(CHANNEL_ID, DATA.to_vec())))) +} + +#[test] +fn decodes_data() { + test_decodes(encoded(), decoded_client()); + test_decodes(encoded(), decoded_server()); +} + +#[test] +fn encodes_data() { + test_encodes(decoded_client(), encoded()); + test_encodes(decoded_server(), encoded()); +} diff --git a/crates/ironrdp-testsuite-core/tests/dvc/data_first.rs b/crates/ironrdp-testsuite-core/tests/dvc/data_first.rs new file mode 100644 index 00000000..aee11e2b --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/dvc/data_first.rs @@ -0,0 +1,185 @@ +use super::*; + +const LENGTH: u32 = 0xC7B; +const CHANNEL_ID: u32 = 0x03; +const PREFIX: [u8; 4] = [0x24, 0x03, 0x7b, 0x0c]; +const DATA: [u8; 12] = [0x71, 0x71, 0x71, 0x71, 0x71, 0x71, 0x71, 0x71, 0x71, 0x71, 0x71, 0x71]; + +// Edge case is when the total length is equal to data length +const EDGE_CASE_LENGTH: u32 = 0x639; +const EDGE_CASE_CHANNEL_ID: u32 = 0x07; +const EDGE_CASE_PREFIX: [u8; 4] = [0x24, 0x7, 0x39, 0x6]; +#[expect(clippy::as_conversions)] +const EDGE_CASE_DATA: [u8; EDGE_CASE_LENGTH as usize] = [ + 0xe0, 0x24, 0xa9, 0xba, 0xe0, 0x68, 0xa9, 0xba, 0x8a, 0x73, 0x41, 0x25, 0x12, 0x12, 0x1c, 0x28, 0x3b, 0xa6, 0x34, + 0x8, 0x8, 0x7a, 0x38, 0x34, 0x2c, 0xe8, 0xf8, 0xd0, 0xef, 0x18, 0xc2, 0xc, 0x27, 0x1f, 0xb1, 0x83, 0x3c, 0x58, + 0x8a, 0x67, 0x1, 0x58, 0x9d, 0x50, 0x8b, 0x8c, 0x60, 0x31, 0x53, 0x55, 0x54, 0xd8, 0x51, 0x32, 0x23, 0x54, 0xd9, + 0xd1, 0x65, 0x54, 0xd6, 0xe0, 0xb6, 0x4b, 0x8b, 0xd1, 0x1a, 0x32, 0x6d, 0x10, 0xed, 0x21, 0x48, 0x8d, 0x1d, 0xa2, + 0x6a, 0x90, 0x14, 0x88, 0x30, 0x18, 0x8a, 0x11, 0x1a, 0x37, 0xc6, 0xa7, 0x32, 0x8b, 0xea, 0x32, 0x44, 0x78, 0xc9, + 0x4e, 0xdb, 0x47, 0x11, 0x53, 0xc6, 0x16, 0xc9, 0x72, 0x55, 0x11, 0x4e, 0x12, 0x55, 0x31, 0xd4, 0x4b, 0x16, 0x71, + 0x7e, 0x32, 0x13, 0x5, 0xb2, 0x5c, 0x9c, 0xa4, 0xab, 0x78, 0xc9, 0x20, 0x88, 0xfb, 0x18, 0xa6, 0xb, 0x44, 0xac, + 0x6, 0x23, 0x4b, 0x2d, 0x12, 0xa3, 0x1a, 0x8b, 0xea, 0x2c, 0x44, 0x69, 0x12, 0xa8, 0xca, 0xa2, 0xc4, 0x64, 0x98, + 0x99, 0x11, 0xd4, 0x5e, 0x8a, 0x51, 0x92, 0x23, 0xa8, 0xbd, 0x34, 0x44, 0xc8, 0x65, 0x44, 0xd7, 0x30, 0x76, 0x11, + 0xea, 0x5a, 0xf7, 0x8b, 0xf1, 0x90, 0xaa, 0x90, 0x14, 0x52, 0x93, 0x4, 0x4c, 0x8b, 0x18, 0xc, 0x55, 0x2b, 0xd1, + 0x5a, 0x4e, 0xea, 0xa9, 0x82, 0x8b, 0xe0, 0xb2, 0x4a, 0x8e, 0x91, 0x62, 0x46, 0xf1, 0x8a, 0x77, 0x90, 0xd4, 0x89, + 0xa2, 0x34, 0x53, 0x15, 0x4f, 0xa4, 0xcf, 0x53, 0x2e, 0x93, 0x18, 0xa2, 0x35, 0x54, 0x51, 0x11, 0xa4, 0x91, 0x16, + 0x26, 0xd8, 0x44, 0xc9, 0x21, 0xde, 0x2f, 0xc6, 0x3b, 0x44, 0x69, 0xd, 0x44, 0xd0, 0x59, 0x99, 0xde, 0x4b, 0x33, + 0x44, 0x44, 0x9a, 0xa2, 0xe5, 0x55, 0xc0, 0x5b, 0x26, 0x55, 0x57, 0x92, 0x0, 0x96, 0xe5, 0x55, 0xb6, 0xa2, 0x6a, + 0x8b, 0xea, 0x3b, 0x45, 0xf5, 0x13, 0x44, 0x88, 0x4c, 0x85, 0x22, 0xc4, 0x46, 0x8e, 0xd1, 0x1a, 0x6d, 0x21, 0x68, + 0x97, 0x21, 0x88, 0xde, 0xa2, 0x94, 0x74, 0x95, 0xcc, 0x62, 0x9d, 0xdb, 0x2d, 0x66, 0x92, 0xc9, 0x5a, 0xa5, 0x90, + 0xa4, 0x46, 0x91, 0xb4, 0x85, 0x22, 0x94, 0x4d, 0x13, 0x37, 0xb, 0x84, 0xd9, 0x2a, 0x9a, 0xaf, 0x32, 0x3a, 0xa8, + 0xdd, 0x11, 0xa4, 0x87, 0x78, 0xbe, 0x91, 0xa8, 0x53, 0xcc, 0x95, 0x22, 0xca, 0x89, 0xb1, 0x8c, 0x92, 0x38, 0x91, + 0x34, 0x8f, 0xa6, 0x88, 0x4c, 0x71, 0x31, 0x8a, 0x50, 0x51, 0x1a, 0x34, 0x48, 0x4e, 0x31, 0x4e, 0x2b, 0x15, 0x62, + 0x96, 0x2c, 0xc6, 0x29, 0xe2, 0x46, 0x21, 0xf, 0x32, 0x32, 0xc0, 0x62, 0x49, 0x12, 0x6a, 0x88, 0xea, 0x2c, 0x46, + 0x88, 0x9a, 0xa4, 0xcd, 0x14, 0xa3, 0x84, 0x64, 0x91, 0xee, 0x28, 0xcb, 0x79, 0x4, 0x50, 0x44, 0x17, 0x9, 0xb1, + 0x42, 0x23, 0x47, 0xbb, 0xc5, 0xf5, 0x31, 0x2, 0x23, 0x45, 0x95, 0x13, 0x63, 0x27, 0xe8, 0xb3, 0x18, 0xa6, 0xa4, + 0x11, 0x31, 0xf6, 0x12, 0xc4, 0x8d, 0x2a, 0x26, 0xde, 0x63, 0x8e, 0x59, 0xd, 0xe5, 0x97, 0x49, 0x98, 0x99, 0x2e, + 0x9d, 0x12, 0x2b, 0xcb, 0x22, 0x9, 0x51, 0xde, 0x2f, 0x4b, 0xa6, 0x11, 0xea, 0x7e, 0xe4, 0x8c, 0x26, 0x1a, 0x49, + 0xb2, 0x23, 0xa8, 0xb1, 0x19, 0x24, 0x19, 0x11, 0xd4, 0x58, 0x8e, 0x2a, 0x43, 0x51, 0xda, 0x23, 0x46, 0x48, 0xee, + 0xa2, 0x66, 0x3, 0x11, 0x2a, 0x23, 0xeb, 0x90, 0xcd, 0x24, 0x1f, 0x1a, 0x16, 0x48, 0xb3, 0x19, 0x2a, 0xf2, 0x67, + 0xeb, 0x48, 0xaf, 0x14, 0x9f, 0xa4, 0xab, 0xd2, 0x6c, 0x35, 0x9a, 0x53, 0x7c, 0x24, 0xb7, 0x79, 0x8f, 0x91, 0x96, + 0xf2, 0x8, 0x9e, 0xc2, 0x65, 0x11, 0x58, 0xe8, 0x4c, 0xc9, 0x15, 0x8d, 0xc4, 0x47, 0x32, 0x18, 0x88, 0xea, 0x35, + 0x82, 0xd5, 0xab, 0xa, 0xe2, 0x46, 0xf7, 0x8b, 0xf7, 0x90, 0x75, 0x8, 0xe9, 0x8a, 0x11, 0x1d, 0x48, 0xf2, 0x4c, + 0xd3, 0x6d, 0xd4, 0x65, 0x51, 0x7a, 0x52, 0x14, 0x22, 0xd2, 0x37, 0xbc, 0x5f, 0xa4, 0x7c, 0x90, 0x74, 0x95, 0xb, + 0x64, 0xc9, 0x21, 0x44, 0x69, 0x1b, 0x8a, 0x13, 0x4a, 0x1, 0x28, 0x9a, 0x64, 0xc1, 0x1a, 0xc4, 0xc0, 0x6a, 0x28, + 0x68, 0x98, 0x5b, 0x26, 0x4c, 0x46, 0x92, 0x1d, 0x22, 0xc3, 0x61, 0xaa, 0xd2, 0x92, 0x44, 0x85, 0x22, 0x65, 0x3c, + 0xe8, 0xb2, 0xa9, 0x2a, 0x39, 0xa, 0x4a, 0x49, 0x3c, 0xba, 0x95, 0xe9, 0x92, 0xc6, 0x98, 0xd5, 0x81, 0x8f, 0x95, + 0x41, 0x94, 0x79, 0x93, 0x14, 0x8c, 0x71, 0x58, 0x27, 0xc9, 0x87, 0x73, 0xf1, 0x66, 0x3c, 0x7d, 0xd3, 0xe6, 0xac, + 0xa, 0xb7, 0x63, 0x18, 0x91, 0x8a, 0xc1, 0x48, 0xdc, 0xc9, 0x4c, 0x1e, 0xc, 0xec, 0x82, 0xd6, 0x97, 0x57, 0x90, + 0xfb, 0x89, 0x88, 0xdf, 0x70, 0xb0, 0xce, 0xae, 0x54, 0x1e, 0x4b, 0x7a, 0x7d, 0x88, 0x8c, 0xd, 0x24, 0x63, 0x43, + 0x7f, 0xc4, 0xee, 0xa3, 0x51, 0xb7, 0x27, 0xb9, 0x73, 0x4f, 0x91, 0xe3, 0xec, 0x79, 0xa9, 0xf9, 0xca, 0x89, 0x28, + 0x2d, 0x44, 0xe0, 0xf3, 0xd1, 0x77, 0x4b, 0xab, 0xc8, 0xb5, 0xc4, 0x74, 0xf, 0xf0, 0xff, 0x3b, 0x8d, 0xd2, 0xe, + 0x22, 0xf6, 0x8c, 0xc, 0xfd, 0xd3, 0x12, 0x31, 0xa9, 0x57, 0xf0, 0x50, 0xd6, 0x45, 0x30, 0xcb, 0x45, 0x82, 0xfe, + 0x9f, 0x63, 0xc3, 0x3, 0x32, 0xa1, 0xa1, 0x83, 0xf, 0xb5, 0x42, 0x85, 0x36, 0xf5, 0x4f, 0x18, 0x34, 0xf9, 0x1e, + 0x3e, 0xc7, 0x9a, 0xb9, 0xd9, 0xe5, 0x51, 0x7a, 0x8c, 0xac, 0x5b, 0x39, 0x98, 0x6e, 0x14, 0x87, 0x1c, 0x67, 0xc7, + 0xa3, 0xed, 0xde, 0x48, 0xd9, 0x47, 0xd0, 0xc5, 0x70, 0xa4, 0x38, 0xe3, 0x60, 0x1a, 0x17, 0x1e, 0x45, 0x1e, 0xdc, + 0x6c, 0x23, 0x1d, 0xc2, 0x90, 0xe3, 0x8d, 0x8c, 0x13, 0x8, 0xa5, 0x86, 0x9, 0x70, 0xca, 0x68, 0xc9, 0x70, 0xa4, + 0x38, 0xe3, 0x45, 0xa9, 0x5e, 0xce, 0x11, 0x23, 0xa5, 0x88, 0xed, 0x32, 0xd5, 0x83, 0xd3, 0x58, 0x31, 0x30, 0x46, + 0x8e, 0xc6, 0xc3, 0x88, 0xf4, 0xd2, 0x28, 0x7e, 0xfd, 0xff, 0xd2, 0xea, 0xf1, 0xf5, 0x71, 0x1d, 0xde, 0x65, 0xb6, + 0xf2, 0xa0, 0x98, 0xd2, 0x3c, 0x19, 0xf4, 0xba, 0xbc, 0x7d, 0x5c, 0x4b, 0x54, 0xab, 0x8d, 0xc0, 0xcc, 0xf0, 0xac, + 0x5d, 0x9a, 0x26, 0xae, 0x7c, 0x99, 0xd9, 0xf6, 0x70, 0x4c, 0xfe, 0xba, 0x7b, 0x70, 0x5f, 0x7f, 0x1a, 0xd7, 0x26, + 0x99, 0xb3, 0x13, 0x67, 0x54, 0xe7, 0x99, 0x22, 0x13, 0xae, 0x24, 0x63, 0x61, 0x52, 0xb5, 0x22, 0x1a, 0xa6, 0xac, + 0x7c, 0x90, 0x4, 0x8c, 0x68, 0xe5, 0x41, 0x36, 0x95, 0xa9, 0xc7, 0x8a, 0x7d, 0xea, 0x6b, 0xd2, 0xea, 0xf2, 0x81, + 0x71, 0x1f, 0x46, 0x4a, 0xe6, 0xb2, 0xa0, 0x64, 0x6d, 0x1a, 0x1b, 0x26, 0xcc, 0x4a, 0x82, 0x6b, 0x44, 0x9a, 0x64, + 0x61, 0x69, 0xa6, 0xd, 0x2e, 0x72, 0xab, 0x2d, 0x4a, 0x6d, 0x9b, 0x31, 0x2a, 0x8, 0xf3, 0x56, 0xc0, 0x7d, 0x9, + 0xfc, 0xb5, 0x15, 0x8c, 0x97, 0xe6, 0xed, 0x2e, 0xaf, 0x1f, 0x57, 0x10, 0xb7, 0x5, 0x80, 0x82, 0x23, 0x89, 0xbb, + 0xd9, 0xbf, 0x49, 0xf8, 0x95, 0x82, 0x74, 0xb6, 0x68, 0x25, 0xf, 0xb1, 0xd1, 0xe3, 0x27, 0x32, 0x6d, 0x4a, 0x70, + 0xd3, 0xee, 0x25, 0x41, 0x88, 0xf3, 0x47, 0x8b, 0x8d, 0x9f, 0xe0, 0x1a, 0x47, 0x39, 0x6, 0xce, 0x37, 0xa, 0x56, + 0xce, 0x34, 0x31, 0x71, 0x7e, 0xab, 0x37, 0x3a, 0xc2, 0x5b, 0xce, 0x5a, 0x5d, 0x5e, 0x40, 0x6e, 0x23, 0xf8, 0xd7, + 0xb3, 0xd5, 0xe6, 0xe0, 0x5e, 0xfa, 0x39, 0xcd, 0xd0, 0x95, 0x25, 0x4b, 0xdd, 0x31, 0x26, 0x41, 0x94, 0xcb, 0xd, + 0x3a, 0xd8, 0x12, 0x93, 0xda, 0x7, 0x4a, 0xa, 0x91, 0x41, 0xc0, 0xc0, 0xa9, 0xaa, 0x28, 0x2a, 0x60, 0x6c, 0xb, + 0x1e, 0x51, 0xe4, 0xc7, 0xaa, 0x35, 0x24, 0x75, 0xd2, 0xea, 0xf1, 0xe7, 0x71, 0x15, 0xdc, 0x4f, 0x12, 0x92, 0xad, + 0xf1, 0xbc, 0xd0, 0x76, 0xd2, 0xea, 0xf1, 0x5b, 0x71, 0x23, 0x93, 0xaa, 0xd5, 0xf1, 0xb1, 0xaa, 0x11, 0x1a, 0x9d, + 0xf4, 0xba, 0xbc, 0x48, 0xdc, 0x46, 0xf6, 0xc8, 0xa7, 0xe3, 0xdc, 0x88, 0xaf, 0x21, 0x9e, 0x25, 0xb2, 0x33, 0xf2, + 0xa9, 0xe2, 0x96, 0xcc, 0x9c, 0x2c, 0xd3, 0xca, 0xf4, 0x2e, 0xe6, 0x17, 0x25, 0xcf, 0x32, 0xd9, 0x19, 0xa1, 0xc0, + 0xac, 0x2f, 0x25, 0xb3, 0x5, 0xab, 0x87, 0xd1, 0x84, 0x4f, 0x91, 0x46, 0x4f, 0x5a, 0x5d, 0x5d, 0xf8, 0xae, 0x24, + 0x12, 0x4c, 0x58, 0xfa, 0x7d, 0xa9, 0x53, 0x8c, 0xe3, 0xdc, 0x8c, 0x3e, 0x48, 0xc2, 0xe1, 0x4b, 0xb9, 0xa6, 0xb, + 0x61, 0x27, 0x39, 0x10, 0xea, 0x93, 0x5c, 0x9f, 0x28, 0x54, 0x2c, 0xa4, 0xa2, 0x23, 0xcd, 0x3, 0xed, 0x4, 0x78, + 0x55, 0x60, 0xce, 0x35, 0x11, 0xcf, 0xb3, 0x9e, 0x46, 0x68, 0x64, 0x25, 0x43, 0x31, 0x11, 0x5c, 0xe3, 0xfc, 0x1c, + 0x36, 0xa2, 0x99, 0x87, 0xe9, 0xcf, 0x23, 0x34, 0x32, 0x11, 0xe6, 0x65, 0x17, 0x9b, 0x99, 0x17, 0xe1, 0xe4, 0x5e, + 0x24, 0x9f, 0xf4, 0xba, 0xbb, 0xca, 0xdc, 0x4c, 0xb7, 0xa1, 0x11, 0x4, 0xd6, 0x9, 0x6c, 0xdc, 0xfb, 0xa5, 0xd5, + 0xde, 0x8e, 0xe2, 0x15, 0xf0, 0x56, 0x95, 0x9c, 0x6, 0xb3, 0xa1, 0x69, 0x3, 0x32, 0xe2, 0x73, 0xe6, 0x26, 0xcc, + 0xd0, 0x88, 0x74, 0x2e, 0x6, 0x5, 0x2c, 0x42, 0xfb, 0x45, 0x5, 0x31, 0x24, 0x4d, 0xd0, 0xfb, 0x37, 0x46, 0x6c, + 0x47, 0xb9, 0xf, 0xcb, 0x35, 0x8d, 0xc1, 0x88, 0x83, 0xd2, 0xed, 0xd5, 0x94, 0xf1, 0x11, 0xdf, 0x2c, 0xb6, 0x29, + 0x39, 0xc6, 0x64, 0x2a, 0xa1, 0x24, 0x61, 0x89, 0x18, 0x7c, 0x91, 0x8c, 0xd3, 0x66, 0xc5, 0x8a, 0xa9, 0xc4, 0xd, + 0x3c, 0x33, 0x50, 0xba, 0x5d, 0xba, 0xad, 0x5e, 0x2d, 0xcf, 0x75, 0x32, 0x90, 0xee, 0xa8, 0xf4, 0xc5, 0x43, 0x50, + 0xa8, 0x62, 0x87, 0xd2, 0x46, 0x34, 0x43, 0xf4, 0x68, 0x88, 0x59, 0x3f, 0xa2, 0xd0, 0x7a, 0x1e, 0x69, 0x45, 0x56, + 0x3d, 0x33, 0x12, 0x70, 0x4c, 0x8d, 0x2c, 0x42, 0x1a, 0x94, 0xd4, 0xb7, 0x62, 0x18, 0x11, 0x1a, 0x5d, 0x5d, 0xe8, + 0xee, 0x23, 0x29, 0xc9, 0x8c, 0x65, 0x60, 0x7b, 0x7b, 0x8c, 0x11, 0x3a, 0x5d, 0xba, 0xcc, 0xde, 0x27, 0x99, 0x58, + 0x8a, 0x8c, 0x69, 0x91, 0xa, 0x2f, 0xb4, 0x55, 0xe, 0x4, 0xc1, 0xd, 0x25, 0x43, 0x42, 0xb2, 0x81, 0x5e, 0x56, 0x5f, + 0x16, 0xc6, 0x28, 0xb4, 0x5e, 0x97, 0x6e, 0xb3, 0x37, 0x89, 0x98, 0xe3, 0x13, 0xbb, 0xc9, 0x49, 0x87, 0xe1, 0x6d, + 0x19, 0xa5, 0xdb, 0xac, 0xcd, 0xe2, 0x32, 0xb3, 0x99, 0x2b, 0xf3, 0x60, 0xd1, 0x20, 0x7e, 0x46, 0xcc, 0x4, 0xc6, + 0x2c, 0xc0, 0x12, 0x26, 0x8, 0xc8, 0xad, 0xfa, 0xf6, 0x3f, 0x10, 0x45, 0xca, 0xe0, 0x9a, 0x3b, 0x4b, 0xb7, 0x57, + 0x33, 0xc4, 0x90, 0x72, 0xab, 0x5a, 0xcc, 0xf, 0x26, 0x42, 0xea, 0x3e, 0x87, 0x2, 0xab, 0xbc, 0x9f, 0x99, 0x23, + 0x19, 0x76, 0xa, 0x8c, 0xc4, 0x8b, 0xc7, 0xf3, 0x95, 0xd2, 0x90, 0xd2, 0xed, 0xd5, 0xcc, 0xf1, 0x31, 0x1d, 0xa7, + 0xa4, 0xba, 0xdd, 0x88, 0x2d, 0x6b, 0xa4, 0x74, 0xbb, 0x75, 0x73, 0x3c, 0x4c, 0x44, 0x42, 0xd, 0x74, 0x61, 0x39, + 0x79, 0x76, 0xa4, 0xa7, 0x3c, 0x4c, 0x80, 0xc4, 0xc8, 0x1a, 0x27, 0x1c, 0xe4, 0xde, 0x8, 0xb2, 0xba, 0x30, 0xa5, + 0x74, 0x36, 0x76, 0xa6, 0x53, 0x9f, 0x33, 0x56, 0x98, 0x88, 0x92, 0x2a, 0xd1, 0x90, 0x1, +]; + +static ENCODED: OnceLock> = OnceLock::new(); +static DECODED_CLIENT: OnceLock = OnceLock::new(); +static DECODED_SERVER: OnceLock = OnceLock::new(); +static EDGE_CASE_ENCODED: OnceLock> = OnceLock::new(); +static EDGE_CASE_DECODED_CLIENT: OnceLock = OnceLock::new(); +static EDGE_CASE_DECODED_SERVER: OnceLock = OnceLock::new(); + +fn encoded() -> &'static Vec { + ENCODED.get_or_init(|| { + let mut result = PREFIX.to_vec(); + result.extend(DATA); + result + }) +} + +fn decoded_client() -> &'static DrdynvcClientPdu { + DECODED_CLIENT.get_or_init(|| { + DrdynvcClientPdu::Data(DrdynvcDataPdu::DataFirst( + DataFirstPdu::new(CHANNEL_ID, LENGTH, DATA.to_vec()) + .with_cb_id_type(FieldType::U8) + .with_sp_type(FieldType::U16), + )) + }) +} + +fn decoded_server() -> &'static DrdynvcServerPdu { + DECODED_SERVER.get_or_init(|| { + DrdynvcServerPdu::Data(DrdynvcDataPdu::DataFirst( + DataFirstPdu::new(CHANNEL_ID, LENGTH, DATA.to_vec()) + .with_cb_id_type(FieldType::U8) + .with_sp_type(FieldType::U16), + )) + }) +} + +fn edge_case_encoded() -> &'static Vec { + EDGE_CASE_ENCODED.get_or_init(|| { + let mut result = EDGE_CASE_PREFIX.to_vec(); + result.append(&mut EDGE_CASE_DATA.to_vec()); + result + }) +} + +fn edge_case_decoded_client() -> &'static DrdynvcClientPdu { + EDGE_CASE_DECODED_CLIENT.get_or_init(|| { + DrdynvcClientPdu::Data(DrdynvcDataPdu::DataFirst( + DataFirstPdu::new(EDGE_CASE_CHANNEL_ID, EDGE_CASE_LENGTH, EDGE_CASE_DATA.to_vec()) + .with_cb_id_type(FieldType::U8) + .with_sp_type(FieldType::U16), + )) + }) +} + +fn edge_case_decoded_server() -> &'static DrdynvcServerPdu { + EDGE_CASE_DECODED_SERVER.get_or_init(|| { + DrdynvcServerPdu::Data(DrdynvcDataPdu::DataFirst( + DataFirstPdu::new(EDGE_CASE_CHANNEL_ID, EDGE_CASE_LENGTH, EDGE_CASE_DATA.to_vec()) + .with_cb_id_type(FieldType::U8) + .with_sp_type(FieldType::U16), + )) + }) +} + +#[test] +fn decodes_data_first() { + test_decodes(encoded(), decoded_client()); + test_decodes(encoded(), decoded_server()); +} + +#[test] +fn encodes_data_first() { + test_encodes(decoded_client(), encoded()); + test_encodes(decoded_server(), encoded()); +} + +#[test] +fn decodes_data_first_edge_case() { + test_decodes(edge_case_encoded(), edge_case_decoded_client()); + test_decodes(edge_case_encoded(), edge_case_decoded_server()); +} + +#[test] +fn encodes_data_first_edge_case() { + test_encodes(edge_case_decoded_client(), edge_case_encoded()); + test_encodes(edge_case_decoded_server(), edge_case_encoded()); +} diff --git a/crates/ironrdp-testsuite-core/tests/dvc/mod.rs b/crates/ironrdp-testsuite-core/tests/dvc/mod.rs new file mode 100644 index 00000000..9c56425e --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/dvc/mod.rs @@ -0,0 +1,27 @@ +use std::sync::OnceLock; + +use ironrdp_core::{Decode, Encode, ReadCursor, WriteCursor}; +use ironrdp_dvc::pdu::{ + CapabilitiesRequestPdu, CapabilitiesResponsePdu, CapsVersion, ClosePdu, CreateRequestPdu, CreateResponsePdu, + CreationStatus, DataFirstPdu, DataPdu, DrdynvcClientPdu, DrdynvcDataPdu, DrdynvcServerPdu, FieldType, +}; + +// TODO: This likely generalizes to many tests and can thus be reused outside of this module. +fn test_encodes(data: &T, expected: &[u8]) { + let mut buffer = vec![0x00; data.size()]; + let mut cursor = WriteCursor::new(&mut buffer); + data.encode(&mut cursor).unwrap(); + assert_eq!(expected, buffer.as_slice()); +} + +// TODO: This likely generalizes to many tests and can thus be reused outside of this module. +fn test_decodes<'a, T: Decode<'a> + PartialEq + core::fmt::Debug>(encoded: &'a [u8], expected: &T) { + let mut src = ReadCursor::new(encoded); + assert_eq!(*expected, T::decode(&mut src).unwrap()); +} + +mod capabilities; +mod close; +mod create; +mod data; +mod data_first; diff --git a/crates/ironrdp-testsuite-core/tests/fuzz_regression.rs b/crates/ironrdp-testsuite-core/tests/fuzz_regression.rs new file mode 100644 index 00000000..1d6e62ea --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/fuzz_regression.rs @@ -0,0 +1,28 @@ +macro_rules! check { + ($oracle:ident) => {{ + use ironrdp_fuzzing::oracles; + + const REGRESSION_DATA_FOLDER: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + concat!("/test_data/fuzz_regression/", stringify!($oracle)) + ); + + println!("Read directory {REGRESSION_DATA_FOLDER}"); + for entry in std::fs::read_dir(REGRESSION_DATA_FOLDER).unwrap() { + let entry = entry.unwrap(); + println!("Check {}", entry.path().display()); + let test_case = std::fs::read(entry.path()).unwrap(); + oracles::$oracle(&test_case); + } + }}; +} + +#[test] +fn check_pdu_decode() { + check!(pdu_decode); +} + +#[test] +fn check_cliprdr_format() { + check!(cliprdr_format); +} diff --git a/crates/ironrdp-testsuite-core/tests/graphics/color_conversion.rs b/crates/ironrdp-testsuite-core/tests/graphics/color_conversion.rs new file mode 100644 index 00000000..1e63d887 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/graphics/color_conversion.rs @@ -0,0 +1,1597 @@ +use ironrdp_graphics::color_conversion::*; +use ironrdp_graphics::image_processing::PixelFormat; + +#[test] +fn to_64x64_ycbcr() { + let input = [0u8; 4]; + + let mut y = [0; 64 * 64]; + let mut cb = [0; 64 * 64]; + let mut cr = [0; 64 * 64]; + to_64x64_ycbcr_tile(&input, 1, 1, 4, PixelFormat::ABgr32, &mut y, &mut cb, &mut cr).unwrap(); +} + +#[ignore] +#[test] +fn rgb_to_ycbcr_converts_large_buffer() { + let xrgb = XRGB_BUFFER.as_ref(); + let expected = YCbCrBuffer { + y: YCBCR_BUFFER_Y.as_ref(), + cb: YCBCR_BUFFER_CB.as_ref(), + cr: YCBCR_BUFFER_CR.as_ref(), + }; + + let mut y = [0; 4096]; + let mut cb = [0; 4096]; + let mut cr = [0; 4096]; + to_64x64_ycbcr_tile(xrgb, 64, 64, 64 * 4, PixelFormat::XRgb32, &mut y, &mut cb, &mut cr).unwrap(); + assert_eq!(expected.y, y.as_slice()); +} + +#[test] +fn ycbcr_to_rgb_converts_one_element_buffer() { + let ycbcr = YCbCrBuffer { + y: &[1], + cb: &[2], + cr: &[3], + }; + let expected = [128, 127, 128, 255]; + + let mut actual = vec![0; 4]; + ycbcr_to_rgba(ycbcr, actual.as_mut()).unwrap(); + assert_eq!(expected.as_ref(), actual.as_slice()); +} + +#[ignore] +#[test] +fn ycbcr_to_rgb_converts_large_buffer() { + let ycbcr = YCbCrBuffer { + y: YCBCR_BUFFER_Y.as_ref(), + cb: YCBCR_BUFFER_CB.as_ref(), + cr: YCBCR_BUFFER_CR.as_ref(), + }; + let expected = XRGB_BUFFER.as_ref(); + + let mut output = vec![0; 4 * 64 * 64]; + ycbcr_to_argb(ycbcr, output.as_mut()).unwrap(); + assert_eq!(expected, output.as_slice()); +} + +// 4.2.4.3.3.7 +const YCBCR_BUFFER_Y: [i16; 4096] = [ + -32, 16, 64, 272, -32, -16, 0, -16, -32, -24, -16, -8, 0, -24, -48, -72, -96, -90, -84, -78, -72, -98, -124, -150, + -176, -192, -208, -224, -240, -256, -272, -288, -304, -304, -304, -304, -304, -336, -368, -400, -432, -450, -468, + -486, -504, -522, -540, -558, -576, -598, -620, -642, -664, -686, -708, -730, -752, -768, -784, -800, -816, -816, + -816, -816, 68, 120, 172, 240, 53, 55, 57, 43, 30, 32, 34, 36, 38, 20, 2, -16, -34, -36, -38, -40, -42, -68, -94, + -120, -146, -149, -152, -186, -221, -228, -234, -241, -247, -255, -262, -269, -276, -303, -330, -357, -384, -404, + -424, -444, -463, -485, -507, -529, -550, -573, -595, -617, -639, -674, -708, -710, -712, -733, -754, -775, -796, + -796, -796, -796, 168, 224, 281, 209, 138, 126, 115, 103, 92, 88, 84, 80, 76, 64, 52, 40, 28, 18, 8, -2, -12, -38, + -64, -90, -116, -106, -95, -148, -201, -199, -196, -193, -190, -205, -219, -233, -247, -270, -292, -314, -336, + -358, -379, -401, -422, -448, -473, -499, -524, -547, -569, -592, -614, -661, -707, -690, -672, -698, -724, -750, + -776, -776, -776, -776, 268, 312, 357, 274, 191, 181, 172, 163, 154, 144, 134, 124, 114, 108, 102, 80, 58, 56, 54, + 52, 50, 24, -2, -44, -86, -63, -38, -94, -150, -138, -126, -146, -165, -171, -176, -198, -219, -237, -254, -271, + -288, -312, -335, -358, -381, -411, -440, -469, -498, -521, -544, -567, -589, -648, -707, -670, -632, -663, -694, + -725, -756, -756, -756, -756, 368, 401, 434, 339, 244, 237, 230, 223, 216, 200, 184, 168, 152, 152, 152, 120, 88, + 94, 100, 106, 112, 86, 60, 2, -56, -19, 19, -40, -98, -77, -55, -98, -140, -137, -133, -162, -190, -203, -215, + -228, -240, -265, -290, -315, -340, -373, -406, -439, -472, -495, -518, -541, -564, -635, -706, -649, -592, -628, + -664, -700, -736, -736, -736, -736, 404, 557, 454, 383, 313, 532, 239, 282, 326, 304, 282, 260, 238, 246, 254, 118, + 238, 196, 154, 32, -90, -88, -86, 76, 238, 242, 247, 28, -191, -232, -273, -123, 29, -63, -155, -151, -146, -164, + -181, -199, -216, -241, -266, -291, -315, -346, -377, -408, -438, -448, -457, -498, -539, -597, -654, -503, -608, + -625, -642, -675, -708, -708, -708, -708, 440, 713, 475, 428, 382, 827, 249, 342, 436, 408, 380, 352, 324, 340, + 356, -140, -124, 42, 208, 214, 220, 250, 280, 406, 532, 504, 476, 352, 229, 125, 21, -147, -314, -245, -176, -139, + -101, -124, -147, -170, -192, -217, -241, -266, -290, -319, -347, -376, -404, -400, -395, -455, -514, -558, -601, + -357, -624, -622, -620, -650, -680, -680, -680, -680, 604, 677, 495, 457, 419, 770, 354, 386, 418, 416, 414, 380, + 346, 258, -342, -302, -6, 288, 582, 604, 626, 588, 550, 688, 826, 829, 833, 724, 616, 482, 347, 181, 15, -139, + -293, -175, -57, -85, -113, -141, -168, -193, -217, -241, -265, -292, -318, -344, -370, -352, -334, -412, -489, + -487, -485, -403, -576, -587, -598, -625, -652, -652, -652, -652, 1280, 1154, 1028, 998, 968, 970, 460, 430, 400, + 424, 448, 408, 368, 432, -528, -208, 112, 534, 956, 994, 1032, 926, 820, 970, 1120, 1155, 1190, 1097, 1004, 839, + 674, 509, 344, 223, 102, 45, -12, -45, -78, -111, -144, -168, -192, -216, -240, -264, -288, -312, -336, -304, -272, + -368, -464, -416, -368, -448, -528, -552, -576, -600, -624, -624, -624, -624, 770, 671, 573, 554, 536, 629, 467, + 464, 462, 492, 523, 490, 457, 281, -406, -101, 204, 599, 995, 1310, 1370, 1297, 1225, 1296, 1368, 1433, 1498, 1403, + 1308, 1185, 1062, 875, 688, 586, 485, 304, 123, -83, -32, -77, -122, -175, -227, -200, -172, -194, -217, -239, + -261, -315, -368, -326, -283, -361, -438, -452, -465, -515, -565, -583, -601, -617, -633, -633, 772, 701, 630, 623, + 616, 545, 474, 499, 524, 561, 599, 572, 546, 131, -283, 6, 296, 665, 1034, 1627, 1708, 1669, 1630, 1623, 1616, + 1711, 1806, 1709, 1612, 1531, 1450, 1241, 1032, 950, 869, 563, 258, -120, 15, -43, -100, -181, -262, -183, -103, + -124, -145, -166, -186, -325, -464, -283, -102, -305, -508, -455, -402, -478, -554, -566, -578, -610, -642, -642, + 774, 730, 687, 675, 664, 620, 577, 581, 586, 598, 610, 590, 571, -147, -97, 209, 516, 794, 1073, 1575, 1822, 1976, + 1875, 1869, 1864, 1989, 2114, 2015, 1916, 1877, 1838, 1607, 1376, 1266, 1156, 902, 137, -61, -3, -121, -238, -124, + -9, -70, -131, -166, -201, -221, -239, -272, -304, -129, -209, -298, -386, -427, -467, -937, -895, -549, -459, + -667, -619, -619, 776, 760, 744, 728, 712, 696, 680, 664, 648, 635, 622, 609, 596, -425, 90, 413, 736, 924, 1112, + 1524, 1936, 2284, 2120, 2116, 2112, 2267, 2422, 2321, 2220, 2223, 2226, 1973, 1720, 1582, 1444, 1242, 16, -2, -20, + 58, 136, -66, -267, -213, -158, -208, -257, -275, -292, -218, -144, 26, -316, -290, -264, -142, -20, 2956, 2860, + -788, -852, -980, -596, -596, 826, 807, 789, 770, 752, 749, 747, 744, 742, 677, 613, 517, 421, -286, 288, 574, 860, + 1081, 1303, 1668, 2034, 2313, 2337, 2344, 2352, 2453, 2554, 2575, 2596, 2507, 2418, 2249, 2080, 1961, 1843, 925, 7, + 40, 74, 748, 654, 451, 250, 48, -154, -108, -62, -112, -161, -29, 104, 44, -271, -275, -278, -842, 1411, 3007, + 3323, 327, -1389, -1197, -493, -493, 876, 855, 834, 813, 792, 803, 814, 825, 836, 720, 605, 681, 758, 110, 487, + 735, 984, 1239, 1494, 1813, 2132, 2343, 2554, 2573, 2592, 2639, 2686, 2829, 2972, 2791, 2610, 2525, 2440, 2341, + 2243, 608, -2, 83, 169, 1438, 1172, 969, 767, 565, 363, 248, 134, 52, -30, -95, -160, -193, -226, -259, -292, 763, + -742, 2290, 1738, -1118, -902, -902, -390, -390, 926, 902, 879, 855, 832, 824, 817, 809, 802, 763, 724, 397, 2375, + 970, 589, 848, 1108, 1396, 1685, 1941, 2198, 2468, 2739, 2785, 2832, 2889, 2946, 3179, 2900, 3059, 2962, 2849, + 2736, 2897, 2546, -365, 309, 206, 871, 1760, 1626, 1471, 1316, 1146, 975, 844, 714, 599, 485, 350, 216, 145, 75, + -356, 750, 2687, 529, -1067, -615, -835, -799, -847, -383, -383, 976, 950, 924, 898, 872, 846, 820, 794, 768, 806, + 844, 882, 1432, 2598, 692, 962, 1232, 1554, 1876, 2070, 2264, 2594, 2924, 2998, 3072, 3139, 3206, 3273, 2316, 3071, + 3314, 3173, 3032, 2941, 1826, -57, 108, 73, 1574, 2083, 2080, 1973, 1866, 1727, 1588, 1441, 1294, 1147, 1000, 796, + 592, 484, 376, 828, 256, 772, -248, -72, -408, 984, -184, -536, -376, -376, 1026, 997, 969, 941, 913, 888, 864, + 840, 816, 762, 709, 768, 1339, 2269, 2176, 1411, 1414, 1677, 1941, 2188, 2436, 2730, 3023, 3157, 3291, 3350, 3409, + 3420, 2152, 3001, 3594, 3403, 3213, 3234, 951, 12, 97, -302, 2883, 2756, 2373, 2312, 2252, 2144, 2036, 1861, 1687, + 1545, 1403, 1254, 1106, 974, 842, 1229, 1105, 21, 217, 46, -381, 1912, 3181, 2765, 301, -723, 1076, 1045, 1015, + 984, 954, 931, 909, 886, 864, 719, 575, 654, 1246, 1685, 3149, 1604, 1596, 1801, 2006, 2307, 2609, 2866, 3123, + 3316, 3510, 3561, 3613, 3568, 1988, 2931, 3875, 3634, 3394, 3527, 76, 81, 86, 859, 3168, 2917, 2666, 2652, 2639, + 2561, 2484, 2282, 2081, 1943, 1806, 1713, 1621, 1464, 1308, 1119, 931, 550, 170, -92, -354, 1560, 3986, 1970, -558, + -558, 1126, 1093, 1060, 1027, 995, 974, 953, 932, 912, 900, 888, -340, 1249, 1757, 2521, 2421, 1810, 2036, 2263, + 2522, 2781, 3066, 3351, 3443, 3537, 3612, 3688, 3476, 2496, 3021, 3803, 3833, 3863, 2844, 33, 134, -21, 2100, 3197, + 3062, 2927, 2944, 2961, 2882, 2804, 2607, 2410, 2309, 2209, 2140, 2071, 1842, 1614, 1329, 1044, 663, 283, 10, -263, + -488, -201, -201, -457, -457, 1176, 1141, 1106, 1071, 1036, 1017, 998, 979, 960, 825, 690, 203, 740, 1573, 1894, + 3239, 2024, 2272, 2521, 2737, 2954, 3010, 3067, 3315, 3564, 3664, 3764, 3384, 3004, 3112, 3732, 3776, 3820, 1905, + -10, 187, -128, 3341, 3226, 3207, 3188, 3236, 3284, 3204, 3124, 2932, 2740, 2676, 2612, 2567, 2522, 2221, 1920, + 1539, 1158, 777, 396, 112, -172, -488, -292, -324, -356, -356, 1194, 1162, 1131, 1100, 1069, 1047, 1026, 973, 920, + 969, 507, 381, 767, 1428, 1834, 2800, 2486, 2347, 2722, 2920, 3118, 3290, 3462, 3266, 3071, 3157, 3243, 3521, 3800, + 3674, 3548, 3710, 3873, 874, 179, 92, 517, 3440, 3291, 3334, 3377, 3403, 3430, 3361, 3292, 3174, 3057, 3004, 2951, + 2761, 2572, 2223, 1874, 1554, 1235, 884, 533, 220, -93, -470, -335, -319, -303, -303, 1212, 1184, 1157, 1129, 1102, + 1078, 1055, 967, 880, 1114, 325, 559, 794, 1284, 1775, 2361, 2948, 2423, 2923, 3103, 3283, 3314, 3346, 3474, 3602, + 3674, 3747, 3659, 3572, 3980, 3877, 3901, 3926, -157, 368, 253, 1674, 3795, 3356, 3461, 3566, 3571, 3577, 3518, + 3460, 3417, 3375, 3332, 3290, 2956, 2623, 2225, 1828, 1570, 1313, 991, 670, 328, -14, -452, -378, -314, -250, -250, + 1230, 1206, 1182, 1158, 1135, 1109, 1083, 1025, 968, 779, 78, 481, 885, 1284, 1939, 2466, 3250, 2627, 2772, 3158, + 3543, 3514, 3486, 3729, 3717, 3775, 3834, 3781, 3728, 3934, 3885, 3916, 2667, 92, 333, 174, 2831, 3702, 3549, 3588, + 3627, 3643, 3659, 3643, 3628, 3676, 3724, 3436, 3149, 2847, 2545, 2275, 2006, 1730, 1454, 1114, 775, 388, 1, -402, + -293, -309, -325, -325, 1248, 1228, 1208, 1188, 1168, 1140, 1112, 1084, 1056, 700, 344, 660, 976, 1284, 2104, 2316, + 3040, 2319, 2110, 2189, 2268, 2691, 3114, 3729, 3832, 3877, 3922, 3903, 3884, 3889, 3894, 3931, 1408, 341, 298, 95, + 3988, 3609, 3742, 3715, 3688, 3715, 3742, 3769, 3796, 3679, 3562, 3285, 3008, 2738, 2468, 2326, 2184, 1890, 1596, + 1238, 880, 448, 16, -352, -208, -304, -400, -400, 1296, 1284, 1272, 1260, 1249, 1165, 1081, 1093, 1106, 232, 382, + 677, 971, 973, 1232, 834, 693, 538, 639, 565, 490, 563, 637, -106, 944, 2358, 3773, 3795, 4074, 3964, 3855, 4337, + 212, 204, 197, 1342, 4023, 3813, 3860, 3811, 3762, 3766, 3771, 3776, 3781, 3604, 3427, 3202, 2977, 2838, 2699, + 2400, 2101, 1982, 1607, 1280, 954, 545, -120, -321, -266, -314, -362, -362, 1344, 1340, 1337, 1333, 1330, 1190, + 1051, 1103, 1156, 20, 933, 950, 967, 919, 872, 889, 906, 805, 705, 733, 761, 740, 720, 668, 616, 328, 40, 1640, + 3752, 3784, 3816, 3208, 40, 580, 97, 2589, 4058, 4018, 3979, 3907, 3836, 3818, 3801, 3784, 3767, 3529, 3292, 3375, + 3458, 3706, 3954, 3754, 3555, 2843, 1619, 1067, 516, 386, -256, -290, -324, -324, -324, -324, 1392, 1364, 1337, + 1310, 1283, 1247, 1212, 969, 982, 1424, 1100, 1079, 1058, 1073, 1088, 815, 799, 1056, 803, 773, 743, 645, 547, 769, + 736, 649, 563, 332, 102, 1939, 4033, 1982, 444, 332, -36, 4076, 4093, 4047, 4001, 3955, 3910, 3870, 3831, 3791, + 3752, 3806, 3861, 3836, 3811, 3678, 3545, 3380, 3216, 3639, 3807, 2342, 1134, 1091, 24, -387, -286, -286, -286, + -286, 1440, 1389, 1338, 1287, 1236, 1305, 1374, 1091, 1320, 1037, 1267, 1208, 1150, 715, 281, 486, 1204, 1564, 901, + 1325, 1750, 1830, 1911, 1383, 344, 459, 574, 817, 548, 351, 666, 757, 336, 340, 856, 4028, 4128, 4076, 4024, 4004, + 3984, 3922, 3861, 3799, 3738, 3828, 3919, 3785, 3652, 3394, 3137, 3007, 2878, 2900, 2923, 3105, 3800, 1284, 1328, + 28, -248, -248, -248, -248, 1456, 1407, 1358, 1309, 1261, 1210, 1159, 1444, 1218, 1265, 33, -655, -1343, -977, + -355, 394, 1401, 1753, 1338, 1739, 2140, 2575, 3010, 3524, 3784, 2536, 1033, 265, 522, 440, 615, 629, 388, 403, + 2211, 4051, 4099, 4078, 4058, 3990, 3922, 3910, 3898, 3886, 3875, 3805, 3736, 3554, 3373, 3126, 2880, 2585, 2291, + 2026, 1762, 2650, 3026, 2303, 2092, 665, -250, -250, -250, -250, 1472, 1425, 1379, 1332, 1286, 1371, 1457, 1030, + -932, -1834, -1712, -1238, -763, -621, 33, 815, 1598, 1943, 1776, 2153, 2531, 2808, 3085, 3362, 3640, 4102, 4052, + 3042, 496, 530, 564, 502, 440, 211, 3055, 3818, 4070, 4081, 4093, 3976, 3860, 3898, 3936, 3974, 4013, 3783, 3553, + 3323, 3094, 2858, 2623, 2420, 2217, 1921, 1626, 915, 2764, 250, 296, 22, -252, -252, -252, -252, 1488, 1443, 1399, + 1371, 1343, 1308, 1530, -408, -1834, -1590, -1089, -813, -536, -281, 485, 1172, 1859, 2132, 2150, 2503, 2857, 3105, + 3352, 3536, 3720, 3875, 3775, 4298, 4054, 2123, 449, 502, 556, 547, 26, 2113, 3945, 4116, 4031, 3946, 3862, 3838, + 3814, 3982, 3894, 3488, 3338, 3140, 2943, 2622, 2302, 2030, 1758, 1496, 1234, 1260, 774, -347, -188, -189, -190, + -222, -254, -254, 1504, 1462, 1420, 1410, 1400, 1246, 1604, -1334, -1712, -1089, -978, -643, -308, 59, 938, 1529, + 2120, 2322, 2524, 2854, 3184, 3402, 3620, 3710, 3800, 3905, 4010, 4019, 4028, 3973, 334, 503, 672, 627, 582, 409, + 236, 2359, 3970, 3917, 3864, 3778, 3692, 3990, 3776, 3194, 3124, 2958, 2792, 2387, 1982, 1641, 1300, 1071, 842, 69, + -192, -176, -160, -144, -128, -192, -256, -256, 1546, 1496, 1447, 1430, 1413, 1627, 1330, -2103, -1184, -820, -712, + -396, -80, 406, 1148, 1714, 2280, 2486, 2692, 2995, 3297, 3467, 3638, 3712, 3787, 3916, 4045, 3918, 4047, 3098, + 357, 656, 699, 198, 466, 381, 297, 376, 200, 1815, 3431, 3568, 3961, 4114, 3755, 3310, 3121, 2804, 2487, 2209, + 1931, 1189, 447, 37, -117, -255, -136, -111, -86, -109, -132, -196, -260, -260, 1588, 1531, 1475, 1450, 1426, 1497, + 33, -1592, -1168, -807, -446, -149, 148, 753, 1358, 1899, 2440, 2650, 2861, 3136, 3411, 3533, 3656, 3715, 3774, + 3927, 4080, 3817, 4066, 2223, 380, 553, 214, 3610, 350, 354, 358, 442, 526, 226, -74, 286, 1158, 1678, 1686, 1634, + 1582, 1114, 646, 239, -168, -31, 107, -228, -51, -66, -80, -46, -12, -74, -136, -200, -264, -264, 1630, 1566, 1502, + 1470, 1439, 1591, -817, -1401, -960, -634, -308, -14, 280, 876, 1472, 1972, 2472, 2718, 2966, 3229, 3492, 3583, + 3674, 3701, 3729, 3794, 3859, 4148, 4181, 708, 563, 418, 1297, 3917, 4234, 2198, 163, 267, 372, 348, 325, 108, 147, + 186, -31, 38, 107, 96, 85, 61, 37, -163, -106, -126, 111, 875, -152, -93, -34, -87, -140, -204, -268, -268, 1672, + 1601, 1530, 1491, 1452, 1685, -1666, -1209, -752, -461, -170, 121, 412, 999, 1586, 2045, 2504, 2787, 3071, 3322, + 3574, 3633, 3693, 3688, 3684, 3661, 3638, 3711, 2760, 473, 746, 283, 2380, 4225, 4022, 4043, 4064, 2141, 218, 215, + 212, 186, 160, 230, 300, 234, 168, 102, 36, -117, -269, 218, 1218, 2025, 2833, 1048, -224, -140, -56, -100, -144, + -208, -272, -272, 1626, 1607, 1589, 1459, 1585, 692, -1480, -1108, -736, -452, -168, 116, 400, 806, 1468, 1938, + 2408, 2703, 2999, 3327, 3655, 3569, 3483, 3620, 3759, 3440, 3121, 1602, 851, 820, 533, 438, 3415, 4252, 4066, 4055, + 4045, 4084, 4124, 2995, 1867, 1068, 269, 62, -145, -38, 69, 704, 1339, 2183, 3028, 2816, 2861, 2953, 2790, -349, + 96, -19, -134, -137, -140, -204, -268, -268, 1580, 1614, 1649, 1427, 1718, -300, -1293, -1007, -720, -443, -166, + 111, 388, 613, 1350, 1831, 2312, 2620, 2928, 3076, 3225, 3249, 3273, 3297, 3322, 3475, 3628, 3333, 1502, 655, 832, + 593, 3938, 4024, 4110, 4068, 4026, 3980, 3934, 3984, 4034, 3998, 3962, 3990, 4018, 3786, 3554, 3610, 3666, 3459, + 3253, 3111, 2969, 2858, 2236, -210, -96, -154, -212, -174, -136, -200, -264, -264, 1662, 1653, 1644, 1619, 1851, + -988, -1267, -986, -704, -402, -100, 10, 120, 404, 944, 1580, 2216, 2504, 2793, 2873, 2954, 2977, 2999, 3086, 3173, + 3238, 3303, 3576, 521, 554, 587, 1772, 3981, 4019, 4058, 4032, 4007, 3971, 3936, 3948, 3961, 3920, 3879, 3806, + 3989, 3866, 3743, 3636, 3529, 3375, 3222, 3069, 2916, 2907, 1362, -119, -64, -113, -162, -147, -132, -196, -260, + -260, 1744, 1692, 1640, 1556, 1472, -1932, -1240, -964, -688, -361, -34, 165, 364, 707, 1050, 1585, 2120, 2389, + 2658, 2671, 2684, 2705, 2726, 2875, 3024, 3001, 2978, 2283, 564, 965, 342, 2951, 4024, 4015, 4006, 3997, 3988, + 3963, 3938, 3913, 3888, 3842, 3796, 3622, 3960, 3946, 3932, 3662, 3392, 3292, 3192, 3028, 2864, 2956, 488, -28, + -32, -72, -112, -120, -128, -192, -256, -256, 1834, 1635, 1692, 1718, 207, -1664, -1230, -925, -619, -285, 50, 256, + 719, 706, 948, 1127, 1562, 1845, 2129, 2236, 2344, 2448, 2551, 2655, 2759, 2739, 2719, 1563, 663, 623, 327, 4207, + 3992, 4013, 4034, 3991, 3948, 3923, 3898, 3873, 3848, 3774, 3701, 3484, 3523, 3726, 3929, 3812, 3695, 3604, 3513, + 3407, 3300, 3349, -441, -232, -22, -48, -74, -100, -126, -174, -222, -222, 1924, 1578, 1745, 1880, -1057, -1395, + -1220, -885, -550, -208, 134, 92, 563, 449, 847, 669, 1004, 1302, 1600, 1802, 2005, 2191, 2377, 2435, 2494, 2477, + 2460, 843, 763, 794, 1337, 3928, 3960, 4011, 4062, 3985, 3908, 3883, 3858, 3833, 3808, 3707, 3607, 3603, 3599, + 3506, 3414, 3706, 3998, 3916, 3835, 3786, 3737, 2207, -346, 77, -12, -24, -36, -80, -124, -156, -188, -188, 1598, + 1585, 1830, 2154, -1874, -1414, -1210, -558, -417, -516, -102, 440, 214, 192, 682, 435, 702, 870, 1039, 1224, 1409, + 1710, 2011, 2039, 2069, 2087, 1849, 795, 766, 596, 2475, 3953, 3896, 3929, 3962, 3915, 3868, 3843, 3818, 3793, + 3768, 3688, 3609, 3577, 3546, 3462, 3379, 3312, 3245, 3364, 3485, 3189, 2893, 857, -155, 33, -34, -48, -62, -108, + -154, -154, -154, -154, 1784, 1849, 1915, 892, -1666, -1177, -1711, -742, -796, -823, 175, -748, 378, 191, 517, + 202, 400, 439, 479, 646, 814, 1229, 1645, 1644, 1644, 1697, 1239, 748, 770, 399, 3613, 3978, 3832, 3847, 3862, + 3845, 3828, 3803, 3778, 3753, 3728, 3669, 3611, 3552, 3494, 3419, 3345, 3174, 3004, 2813, 2623, 2592, 2562, -237, + 37, -10, -56, -72, -88, -136, -184, -152, -120, -120, 1802, 1900, 2255, -286, -1291, -1130, -713, -393, -327, -387, + -445, 200, -179, 436, 27, -46, -118, 203, 270, 384, 498, 686, 874, 998, 1123, 1253, 1128, 794, 717, 1161, 3654, + 3843, 3776, 3789, 3802, 3783, 3764, 3617, 3726, 3691, 3656, 3596, 3536, 3476, 3417, 3341, 3266, 3078, 2891, 2687, + 2484, 2617, 1982, -29, 8, 12, 18, -18, -54, 6, 66, -30, -126, -126, 1820, 1696, 2084, -2232, -1939, -571, -1763, + -1835, -1394, -462, -553, -388, -223, -1111, -462, -37, -124, -32, -451, -134, 183, 143, 104, 353, 602, 809, 1017, + 841, 665, 1924, 3696, 3708, 3720, 3731, 3742, 3721, 3700, 3431, 3674, 3629, 3584, 3523, 3462, 3401, 3341, 3264, + 3187, 2982, 2778, 2562, 2346, 2386, 891, -77, -21, 35, 92, 36, -20, -108, -196, -164, -132, -132, 1710, 1955, 1177, + -2834, -956, -2076, -2173, -365, -1885, -1353, -821, -1600, -844, -1250, -887, -653, -674, -555, -436, -636, -325, + -304, -282, -101, -175, 493, 906, 871, 580, 2767, 3674, 3653, 3632, 3657, 3682, 3627, 3572, 3437, 3558, 3535, 3512, + 3450, 3388, 3326, 3264, 3186, 3108, 2902, 2697, 2500, 2304, 2219, 343, 179, 270, 154, 38, -6, -50, -110, -170, + -154, -138, -138, 1600, 1959, -242, -2667, -2020, -2557, -2582, -1455, 696, 316, 960, 2052, 2120, 1940, 1760, 1292, + 824, -310, -932, -1394, -832, -750, -668, -298, -440, 434, 796, 902, 496, 3610, 3652, 3598, 3544, 3583, 3622, 3533, + 3444, 3443, 3442, 3441, 3440, 3377, 3314, 3251, 3188, 3109, 3030, 2823, 2616, 2439, 2262, 2053, -204, 179, 50, 17, + -16, -48, -80, -112, -144, -144, -144, -144, 1956, 1852, -2091, -3026, -1145, 322, 2045, 1672, 1555, 1328, 1614, + 1916, 1706, 1622, 1282, 1502, 1466, 1301, 1393, 940, -792, -1548, -769, -821, -617, 926, 934, 909, 1397, 3323, + 3456, 3446, 3436, 3393, 3351, 3388, 3426, 3374, 3321, 3445, 3313, 3265, 3217, 3153, 3090, 2998, 2906, 2686, 2467, + 2291, 2115, 1283, -61, 137, 79, 37, -5, -37, -69, -101, -133, -133, -133, -133, 1800, 1746, 669, 1992, 1779, 1665, + 1552, 1727, 1390, 1317, 1245, 1269, 1293, 1560, 1316, 1456, 1084, 1121, 1158, 971, 1297, 726, -869, -1344, -794, + 1419, 1072, 917, 2299, 3036, 3261, 3294, 3328, 3204, 3080, 3244, 3409, 3305, 3201, 3449, 3186, 3153, 3121, 3056, + 2992, 2887, 2783, 2550, 2318, 2143, 1968, 513, 82, 95, 108, 57, 6, -26, -58, -90, -122, -122, -122, -122, 1516, + 1832, 1637, 1905, 1406, 1344, 1283, 1590, 1641, 1466, 1292, 1277, 1263, 1386, 1254, 1314, 1118, 1116, 1115, 906, + 953, 1160, 1111, 117, -363, 807, 698, 701, 2240, 3325, 2362, 2934, 3252, 2998, 2745, 2924, 3103, 3156, 2953, 3277, + 3091, 3057, 3024, 2959, 2894, 2776, 2659, 2414, 2169, 2075, 1981, 255, 65, 69, 73, 45, 17, -15, -47, -79, -111, + -111, -111, -111, 1744, 1662, 1581, 1563, 1546, 1536, 1527, 1453, 1380, 1359, 1339, 1286, 1234, 1213, 1193, 1172, + 1152, 1112, 1073, 1097, 1122, 826, 1043, 1067, 1092, 964, 837, 741, 2182, 2078, 2487, 2831, 2664, 2793, 2923, 2860, + 2798, 3007, 2705, 3106, 2996, 2962, 2928, 2862, 2796, 2666, 2536, 2278, 2020, 1751, 1482, -259, 48, 43, 38, 33, 28, + -4, -36, -68, -100, -100, -100, -100, 1684, 1640, 1596, 1584, 1573, 1543, 1514, 1452, 1391, 1360, 1329, 1282, 1236, + 1213, 1191, 1168, 1146, 1107, 1070, 1064, 1058, 920, 1038, 996, 955, 924, 895, 881, 1635, 1679, 2235, 2439, 2132, + 2451, 2772, 2580, 2644, 2714, 2528, 2742, 2701, 2828, 2699, 2570, 2442, 2383, 2324, 2105, 1887, 1733, 811, -79, 55, + 63, 71, 47, 23, -7, -37, -67, -97, -113, -129, -129, 1624, 1618, 1612, 1606, 1601, 1551, 1501, 1451, 1402, 1361, + 1320, 1279, 1239, 1214, 1189, 1164, 1140, 1103, 1067, 1031, 995, 1014, 1034, 926, 818, 885, 953, 1021, 1089, 1024, + 1472, 2048, 2112, 2110, 2109, 2044, 2491, 2421, 2352, 2379, 2406, 2694, 2471, 2279, 2088, 2100, 2113, 1933, 1754, + 1715, 140, 101, 62, 83, 104, 61, 18, -10, -38, -66, -94, -126, -158, -158, 1724, 1788, 1852, 1692, 1532, 1494, + 1456, 1418, 1381, 1346, 1311, 1276, 1241, 1214, 1187, 1160, 1134, 1099, 1064, 1030, 995, 996, 998, 935, 873, 878, + 883, 793, 702, 657, 1125, 1832, 2284, 1193, 1638, 1796, 2209, 2320, 2176, 2239, 2047, 2560, 2562, 1892, 1734, 1673, + 1613, 1745, 1621, 1153, -83, -7, 69, 71, 73, 43, 13, -13, -39, -65, -91, -139, -187, -187, 1824, 1702, 1580, 1522, + 1464, 1438, 1412, 1386, 1360, 1331, 1302, 1273, 1244, 1215, 1186, 1157, 1128, 1095, 1062, 1029, 996, 979, 962, 945, + 928, 871, 814, 821, 828, 803, 1290, 1617, 1944, 2068, 1168, 1292, 1416, 1708, 1488, 1844, 1688, 2171, 2142, 1249, + 1380, 1503, 1626, 1045, -48, 79, 206, 141, 76, 59, 42, 25, 8, -16, -40, -64, -88, -152, -216, -216, 1688, 1615, + 1542, 1501, 1460, 1429, 1398, 1367, 1336, 1310, 1284, 1258, 1232, 1206, 1180, 1154, 1128, 1093, 1058, 1023, 988, + 969, 950, 931, 912, 862, 812, 794, 776, 596, 672, 972, 1272, 330, 924, 1038, 1152, 1298, 1444, 1910, 1608, 1532, + 1200, 516, 344, 260, 176, 252, 72, 123, 174, 129, 84, 65, 46, 27, 8, -18, -44, -70, -96, -144, -192, -192, 1552, + 1528, 1504, 1480, 1456, 1420, 1384, 1348, 1312, 1289, 1266, 1243, 1220, 1197, 1174, 1151, 1128, 1091, 1054, 1017, + 980, 959, 938, 917, 896, 853, 810, 767, 724, 645, 566, 583, 600, 640, 680, 528, 376, 376, 888, 1464, 1016, 637, + 258, 295, 332, 297, 262, 227, 192, 167, 142, 117, 92, 71, 50, 29, 8, -20, -48, -76, -104, -136, -168, -168, 1544, + 1521, 1498, 1475, 1452, 1411, 1370, 1329, 1288, 1268, 1248, 1228, 1208, 1188, 1168, 1148, 1128, 1089, 1050, 1011, + 972, 949, 926, 903, 880, 844, 808, 772, 736, 678, 620, 610, 600, 614, 628, 546, 464, 238, 2060, 1690, 1576, 1710, + 308, 314, 320, 286, 252, 218, 184, 163, 142, 121, 100, 77, 54, 31, 8, -22, -52, -82, -112, -128, -144, -144, 1536, + 1514, 1492, 1470, 1448, 1402, 1356, 1310, 1264, 1247, 1230, 1213, 1196, 1179, 1162, 1145, 1128, 1087, 1046, 1005, + 964, 939, 914, 889, 864, 835, 806, 777, 748, 711, 674, 637, 600, 588, 576, 564, 552, 612, 160, 1916, 1112, 223, + 358, 333, 308, 275, 242, 209, 176, 159, 142, 125, 108, 83, 58, 33, 8, -24, -56, -88, -120, -120, -120, -120, 1536, + 1514, 1492, 1470, 1448, 1402, 1356, 1310, 1264, 1247, 1230, 1213, 1196, 1179, 1162, 1145, 1128, 1087, 1046, 1005, + 964, 939, 914, 889, 864, 835, 806, 777, 748, 711, 674, 637, 600, 588, 576, 564, 552, 644, 480, 108, 504, 159, 326, + 317, 308, 275, 242, 209, 176, 159, 142, 125, 108, 83, 58, 33, 8, -24, -56, -88, -120, -120, -120, -120, 1536, 1514, + 1492, 1470, 1448, 1402, 1356, 1310, 1264, 1247, 1230, 1213, 1196, 1179, 1162, 1145, 1128, 1087, 1046, 1005, 964, + 939, 914, 889, 864, 835, 806, 777, 748, 711, 674, 637, 600, 588, 576, 564, 552, 420, 288, 348, 408, 351, 294, 301, + 308, 275, 242, 209, 176, 159, 142, 125, 108, 83, 58, 33, 8, -24, -56, -88, -120, -120, -120, -120, 1536, 1514, + 1492, 1470, 1448, 1402, 1356, 1310, 1264, 1247, 1230, 1213, 1196, 1179, 1162, 1145, 1128, 1087, 1046, 1005, 964, + 939, 914, 889, 864, 835, 806, 777, 748, 711, 674, 637, 600, 588, 576, 564, 552, 420, 288, 348, 408, 351, 294, 301, + 308, 275, 242, 209, 176, 159, 142, 125, 108, 83, 58, 33, 8, -24, -56, -88, -120, -120, -120, -120, +]; + +// 4.2.4.3.5 11.5 +const YCBCR_BUFFER_CB: [i16; 4096] = [ + 1728, 1730, 1732, 1734, 1736, 1738, 1740, 1742, 1744, 1740, 1736, 1732, 1728, 1796, 1864, 1804, 1744, 1754, 1764, + 1774, 1784, 1794, 1804, 1814, 1824, 1774, 1724, 1802, 1880, 1814, 1748, 1810, 1872, 1878, 1884, 1890, 1896, 1910, + 1924, 1938, 1952, 1938, 1924, 1910, 1896, 1914, 1932, 1950, 1968, 1974, 1980, 1986, 1992, 1998, 2004, 2010, 2016, + 2016, 2016, 2016, 2016, 2016, 2016, 2016, 1710, 1697, 1684, 1704, 1723, 1726, 1730, 1733, 1737, 1738, 1740, 1741, + 1743, 1758, 1774, 1757, 1741, 1762, 1783, 1788, 1793, 1774, 1755, 1784, 1813, 1817, 1821, 1825, 1829, 1857, 1885, + 1881, 1877, 1849, 1821, 1857, 1894, 1904, 1914, 1924, 1935, 1928, 1922, 1915, 1909, 1922, 1936, 1949, 1963, 1974, + 1985, 1997, 2008, 2009, 2011, 2012, 2014, 2017, 2020, 2023, 2026, 2026, 2026, 2026, 1692, 1664, 1637, 1674, 1711, + 1715, 1720, 1725, 1730, 1737, 1744, 1751, 1758, 1721, 1684, 1711, 1738, 1770, 1802, 1802, 1802, 1754, 1706, 1754, + 1802, 1860, 1918, 1848, 1778, 1900, 2022, 1952, 1882, 1820, 1759, 1825, 1892, 1898, 1905, 1911, 1918, 1919, 1920, + 1921, 1922, 1931, 1940, 1949, 1958, 1974, 1991, 2008, 2025, 2021, 2018, 2015, 2012, 2018, 2024, 2030, 2036, 2036, + 2036, 2036, 1674, 1631, 1590, 1644, 1698, 1704, 1710, 1716, 1723, 1735, 1748, 1760, 1773, 1763, 1754, 1760, 1767, + 1794, 1821, 1800, 1779, 1830, 1881, 1900, 1919, 2047, 2175, 2015, 1855, 1879, 1903, 1927, 1951, 1759, 1824, 1857, + 1890, 1892, 1895, 1898, 1901, 1909, 1918, 1926, 1935, 1939, 1944, 1948, 1953, 1974, 1997, 2019, 2041, 2033, 2025, + 2017, 2010, 2019, 2028, 2037, 2046, 2046, 2046, 2046, 1656, 1599, 1543, 1614, 1686, 1693, 1701, 1708, 1716, 1734, + 1752, 1770, 1788, 1806, 1824, 1810, 1796, 1818, 1840, 2054, 2268, 1650, 1032, 510, -12, -70, -128, 390, 908, 1602, + 2296, 2158, 2020, 1699, 1890, 1889, 1888, 1887, 1886, 1885, 1884, 1900, 1916, 1932, 1948, 1948, 1948, 1948, 1948, + 1975, 2003, 2030, 2058, 2045, 2033, 2020, 2008, 2020, 2032, 2044, 2056, 2056, 2056, 2056, 1590, 1570, 1551, 1612, + 1673, 1580, 1743, 1713, 1685, 1672, 1660, 1711, 1763, 1694, 1626, 1941, 2001, 2060, 583, -654, -1891, -2046, -2201, + -2084, -1967, -2049, -2131, -2053, -1975, -1751, -1527, 41, 1609, 2374, 1859, 2000, 1886, 1899, 1912, 1909, 1907, + 1900, 1894, 1919, 1945, 1944, 1944, 1943, 1943, 1967, 1992, 2017, 2042, 2033, 2024, 2014, 2006, 2017, 2028, 2039, + 2050, 2050, 2050, 2050, 1524, 1542, 1560, 1610, 1661, 1467, 1785, 1719, 1654, 1611, 1568, 1653, 1738, 1839, 1940, + 793, -866, -2050, -2210, -2082, -1954, -1902, -1850, -1862, -1874, -1980, -2086, -1936, -1786, -1776, -1766, -1820, + -1874, -535, 1829, 2112, 1884, 1911, 1939, 1934, 1930, 1901, 1872, 1907, 1942, 1941, 1940, 1939, 1938, 1960, 1982, + 2004, 2027, 2021, 2015, 2009, 2004, 2014, 2024, 2034, 2044, 2044, 2044, 2044, 1586, 1641, 1697, 1704, 1712, 1578, + 1699, 1661, 1623, 1613, 1604, 1642, 1681, 1791, -402, -2036, -1877, -2144, -1899, -1942, -1985, -1918, -1851, + -1880, -1909, -1959, -2009, -1931, -1853, -1801, -1749, -1617, -1485, -1940, -1882, 96, 2074, 1971, 1869, 1895, + 1921, 1885, 1850, 1894, 1939, 1937, 1936, 1934, 1933, 1952, 1972, 1991, 2011, 2009, 2006, 2004, 2002, 2011, 2020, + 2029, 2038, 2038, 2038, 2038, 1136, 1229, 1322, 1287, 1252, 1433, 1614, 1603, 1592, 1616, 1640, 1632, 1624, 2256, + -1720, -1792, -1864, -1982, -2100, -2058, -2016, -1934, -1852, -1898, -1944, -1938, -1932, -1926, -1920, -1826, + -1732, -1670, -1608, -1552, -1496, -1664, -1320, 2288, 1800, 1856, 1912, 1870, 1828, 1882, 1936, 1934, 1932, 1930, + 1928, 1945, 1962, 1979, 1996, 1997, 1998, 1999, 2000, 2008, 2016, 2024, 2032, 2032, 2032, 2032, 1552, 1625, 1698, + 1675, 1652, 1645, 1638, 1615, 1592, 1611, 1630, 1681, 1733, 1146, -2001, -1788, -1830, -1925, -2019, -2050, -2080, + -1987, -1893, -1896, -1898, -1897, -1895, -1861, -1827, -1780, -1732, -1668, -1604, -1616, -1627, -1879, -594, + 2063, 1903, 2016, 1873, 2132, 1880, 1884, 1888, 1921, 1955, 1941, 1927, 1926, 1925, 1956, 1987, 2006, 2025, 2044, + 2063, 1995, 1927, 2099, 2015, 2095, 2175, 2175, 1456, 1509, 1562, 1551, 1540, 1601, 1662, 1627, 1592, 1606, 1621, + 1731, 1842, 36, -2281, -1783, -1796, -1867, -1938, -2041, -2144, -2039, -1934, -1893, -1852, -1855, -1857, -1796, + -1734, -1733, -1731, -1666, -1600, -1679, -1758, -1837, 645, 2094, 2007, 1920, 1322, 2139, 1933, 1886, 1840, 1909, + 1979, 1952, 1926, 1907, 1888, 1933, 1978, 2015, 2052, 2089, 2126, 1982, 1838, 2174, 1998, 2158, 2318, 2318, 1488, + 1521, 1554, 1555, 1556, 1589, 1622, 1607, 1592, 1569, 1547, 1701, 1855, -994, -2050, -1826, -1858, -1906, -1953, + -2017, -2080, -1996, -1911, -1859, -1806, -1813, -1820, -1731, -1641, -1686, -1731, -1680, -1628, -1679, -1729, + -2195, 1947, 2125, 2047, 944, -2205, 114, 2177, 2144, 1856, 1913, 1970, 1963, 1957, 1936, 1915, 1926, 1937, 1992, + 2047, 2182, 2061, 2337, 2613, 1817, 2301, 2157, 2269, 2397, 1520, 1533, 1546, 1559, 1572, 1577, 1582, 1587, 1592, + 1533, 1474, 1671, 1868, -2023, -1818, -1869, -1920, -1944, -1968, -1992, -2016, -1952, -1888, -1824, -1760, -1771, + -1782, -1665, -1548, -1639, -1730, -1693, -1656, -1678, -1699, -1017, 2226, 1644, 2087, -287, -2148, -2167, -1674, + 611, 2384, 2173, 1962, 1975, 1988, 1965, 1942, 1919, 1896, 1969, 2042, 2019, 1484, -1916, -1220, 2484, 1068, -916, + 1708, 1964, 1504, 1515, 1526, 1537, 1548, 1551, 1554, 1557, 1560, 1582, 1604, 1786, 689, -2139, -1895, -1907, + -1918, -1927, -1935, -1944, -1952, -1879, -1805, -1732, -1658, -1628, -1597, -1550, -1503, -1509, -1514, -1519, + -1524, -1528, -1786, 147, 2080, 1995, 2422, -2095, -2003, -2035, -1810, -1665, -1776, -190, 1397, 2536, 2139, 2122, + 2105, 2328, 2295, 2204, 2113, 2870, -213, -1669, -1077, -1237, -1653, -1589, 2059, 1931, 1488, 1497, 1506, 1515, + 1524, 1525, 1526, 1527, 1528, 1631, 1735, 1902, -490, -2255, -1971, -1944, -1916, -1909, -1902, -1895, -1888, + -1805, -1722, -1639, -1556, -1484, -1411, -1435, -1458, -1378, -1297, -1345, -1392, -1377, -1873, 1311, 1935, 1834, + 1734, -2622, -2370, -2158, -1945, -1893, -1840, -2040, -2239, -2023, -782, -281, 220, 433, 134, -377, -888, -1655, + -1398, -1166, -934, -1374, -1302, -726, 2410, 1898, 1472, 1479, 1486, 1493, 1500, 1499, 1498, 1497, 1496, 1600, + 1705, 1666, -933, -1475, -2016, -1965, -1914, -1892, -1869, -1847, -1824, -1732, -1639, -1547, -1454, -1388, -1322, + -1192, -1317, -1151, -1241, -1251, -1260, -1546, -1576, 2459, 1885, 2057, 182, -2430, -2225, -2089, -1953, -1929, + -1904, -1906, -1908, -2150, -1879, -1836, -1793, -1670, -1803, -1646, -1489, -1492, -1239, -1335, -1431, -1335, + -1495, 681, 2345, 2089, 1456, 1461, 1466, 1471, 1476, 1473, 1470, 1467, 1464, 1570, 1676, 1174, -1888, -950, -2060, + -1986, -1912, -1874, -1836, -1798, -1760, -1658, -1556, -1454, -1352, -1292, -1232, -1204, -1688, -1180, -1184, + -1156, -1128, -1203, -254, 2071, 1836, 2281, -1370, -2237, -2080, -2020, -1960, -1964, -1968, -2028, -2088, -2020, + -1952, -1855, -1758, -1725, -1692, -1635, -1578, -1329, -1592, -1504, -1416, -1040, -1688, 2088, 2280, 2280, 1428, + 1439, 1450, 1461, 1472, 1463, 1454, 1493, 1533, 1512, 1748, -161, -2069, -1347, -1138, -1776, -1902, -1849, -1795, + -1709, -1623, -1545, -1467, -1357, -1247, -1199, -1150, -1197, -1756, -1247, -994, -1013, -1032, -1203, 930, 2023, + 1837, 2238, -2481, -2288, -1838, -1800, -1762, -1836, -1909, -1955, -2001, -1983, -1964, -1909, -1854, -1831, + -1807, -1750, -1693, -1540, -1642, -1526, -1410, -638, -122, 774, 1926, 1926, 1400, 1417, 1434, 1451, 1469, 1454, + 1439, 1520, 1602, 1455, 1820, -1239, -1737, -1744, -727, -1822, -1892, -1823, -1753, -1619, -1485, -1432, -1378, + -1260, -1142, -1105, -1067, -1189, -1823, -1314, -804, -870, -936, -1203, 2115, 1976, 1838, 915, -2055, -1570, + -1596, -1580, -1563, -1707, -1850, -1882, -1913, -1945, -1976, -1963, -1949, -1936, -1922, -1865, -1807, -1750, + -1692, -1548, -1404, -1004, -92, 996, 2084, 2084, 1372, 1395, 1418, 1441, 1465, 1444, 1424, 1483, 1543, 1765, 1732, + -2205, -1534, -1613, -1180, -1276, -1882, -1765, -1647, -1562, -1476, -1303, -1129, -1115, -1101, -995, -888, + -1054, -1731, -1397, -806, -711, -872, -307, 2051, 1929, 2063, -152, -1598, -1348, -1354, -1328, -1301, -1418, + -1535, -1601, -1666, -1731, -1796, -1825, -1853, -1881, -1909, -1884, -1858, -1768, -1678, -1570, -1462, -1434, + 1154, 2402, 1858, 1858, 1344, 1373, 1403, 1432, 1462, 1435, 1409, 1446, 1484, 1564, 621, -1891, -1842, -1738, + -1633, -729, -1872, -1707, -1541, -1504, -1466, -1429, -1391, -1226, -1060, -885, -709, -918, -1638, -1479, -807, + -552, -808, 590, 1988, 1882, 2288, -1218, -1140, -1126, -1112, -1075, -1038, -1129, -1220, -1319, -1418, -1517, + -1616, -1686, -1756, -1826, -1896, -1902, -1908, -1786, -1664, -1592, -1520, -1864, 2400, 2016, 2144, 2144, 1348, + 1373, 1399, 1424, 1450, 1463, 1477, 1491, 1505, 1728, -607, -1839, -1791, -1737, -1683, -1005, -1350, -1711, -1560, + -1521, -1481, -1384, -1286, -1381, -1475, -1209, -943, -613, -794, -798, -801, -613, -680, 1364, 1872, 1932, 1481, + -1151, -967, -927, -886, -869, -852, -931, -1009, -1062, -1115, -1232, -1348, -1522, -1696, -1806, -1915, -1901, + -1887, -1793, -1698, -1604, -1766, -744, 2326, 2134, 2198, 2198, 1352, 1373, 1395, 1417, 1439, 1492, 1546, 1536, + 1526, 1893, -1835, -1787, -1739, -1736, -1732, -1280, -828, -1715, -1578, -1537, -1495, -1338, -1181, -1024, -866, + -765, -664, -563, -973, -372, -283, -418, -552, 2138, 1757, 1983, 674, -1084, -793, -727, -660, -663, -665, -732, + -798, -805, -811, -946, -1080, -1358, -1635, -1785, -1934, -1900, -1865, -1799, -1732, -1616, -2012, 376, 2252, + 2252, 2252, 2252, 1356, 1373, 1391, 1409, 1427, 1425, 1423, 1501, 1579, 906, -1815, -1703, -1848, -1911, -1717, + -1636, -786, -1687, -1820, -1713, -1606, -1373, -1140, -923, -705, -657, -609, -385, -417, -235, -309, -479, 376, + 1968, 1769, 2034, -5, -841, -652, -607, -562, -585, -607, -661, -715, -740, -764, -964, -1164, -1434, -1703, -1844, + -1985, -1979, -1972, -1885, -1798, -2012, -2226, 2152, 2178, 2194, 2210, 2210, 1360, 1374, 1388, 1402, 1416, 1358, + 1300, 1466, 1632, -81, -1794, -1619, -1956, -2085, -1702, -1991, -744, -891, -526, -353, -180, -383, -586, -821, + -1056, -805, -554, -463, -372, -353, -334, -539, 1304, 1799, 1782, 2085, -684, -597, -510, -487, -464, -506, -548, + -590, -632, -674, -716, -982, -1248, -1509, -1770, -1903, -2036, -2057, -2078, -1971, -1864, -1896, -1416, 2392, + 2104, 2136, 2168, 2168, 1346, 1358, 1371, 1383, 1396, 1395, 1393, 1552, 1711, -1178, -1763, -2204, -1364, -465, + 690, 1941, 1913, 1747, 1837, 1816, 1794, 1888, 1983, 1773, 1564, 548, -468, -299, -387, -393, -398, -148, 1895, + 1920, 1946, 1284, -402, -398, -394, -422, -450, -479, -508, -569, -630, -723, -816, -1069, -1321, -1698, -2075, + -2083, -2092, -2131, -2169, -2032, -1894, -2028, 142, 2280, 2114, 2082, 2050, 2050, 1332, 1343, 1354, 1365, 1377, + 1432, 1487, 1382, 1278, -1763, -195, 1308, 1788, 1667, 1547, 1522, 1498, 1569, 1641, 1681, 1721, 1600, 1480, 1552, + 1624, 1901, 2179, 1145, -401, -432, -462, -12, 1974, 1786, 2111, 484, -119, -199, -278, -357, -436, -452, -468, + -548, -627, -771, -915, -899, -882, -607, -331, -471, -611, -1436, -2260, -2092, -1924, -2160, 1700, 2168, 2124, + 2028, 1932, 1932, 1318, 1327, 1337, 1347, 1357, 1405, 1453, 1420, 1389, 1380, 1628, 1748, 1356, 1495, 1635, 1631, + 1627, 1551, 1733, 1690, 1647, 1728, 1809, 1730, 1652, 1686, 1722, 1949, 1920, 873, -430, 363, 1925, 1764, 1860, + 147, -29, -96, -162, -292, -422, -425, -428, -559, -689, -372, -310, -281, -251, -572, -891, -859, -827, -565, + -303, -1081, -1858, -1636, 2170, 2296, 2166, 2118, 2070, 2070, 1304, 1312, 1321, 1329, 1338, 1378, 1419, 1459, + 1500, 1452, 1404, 1420, 1436, 1580, 1724, 1484, 1244, 1022, 1313, 1187, 1062, 1088, 1115, 1397, 1680, 1728, 1777, + 1729, 1682, 1922, 1651, 1763, 1876, 1742, 1609, -189, 62, 8, -45, -227, -408, -398, -387, -569, -750, -228, -217, + -431, -644, -1048, -1451, -1503, -1554, -1230, -905, -581, -256, -856, 1616, 1912, 2208, 2208, 2208, 2208, 1290, + 1304, 1320, 1335, 1350, 1377, 1404, 1271, 1395, 1525, 1655, 1769, 1884, 1802, 1720, 1430, 1141, 1026, 1168, 1038, + 908, 700, 492, 331, 172, 873, 1575, 1525, 1731, 1991, 1739, 1774, 1811, 1914, 993, -120, 48, -75, -197, -272, -346, + -409, -471, -326, -180, -215, -505, -811, -1117, -1275, -1432, -1637, -1842, -1825, -1552, -1248, -686, 1194, 1026, + 1610, 2194, 2194, 2194, 2194, 1276, 1297, 1319, 1341, 1363, 1376, 1390, 1340, 1802, 1854, 1907, 1863, 1820, 1768, + 1717, 1377, 1038, 1031, 1024, 889, 755, 568, 381, 290, 200, 19, -162, 553, 1781, 2060, 1827, 1786, 1746, 2086, 378, + -50, 35, -157, -349, -317, -284, -420, -555, -338, -121, -457, -792, -935, -1078, -1245, -1412, -1515, -1617, + -1908, -1687, -1658, -1116, 1964, 1972, 2076, 2180, 2180, 2180, 2180, 1262, 1290, 1318, 1347, 1375, 1359, 1344, + 1632, 1921, 1927, 1934, 1877, 1820, 1702, 1585, 1260, 935, 907, 880, 724, 569, 436, 302, 217, 132, 44, -43, -99, + 102, 801, 2011, 1878, 1745, 1426, 2131, 916, -43, -192, -341, -394, -446, -463, -479, -239, -255, -523, -791, -963, + -1135, -1520, -1648, -1761, -1873, -1447, -2046, -1828, -1354, 2254, 2278, 2222, 2166, 2166, 2166, 2166, 1248, + 1283, 1318, 1353, 1388, 1343, 1298, 1925, 2040, 2001, 1962, 1891, 1820, 1637, 1454, 1143, 832, 784, 736, 560, 384, + 304, 224, 144, 64, 70, 76, 18, -40, 54, 1684, 1714, 1744, 1790, 1836, 1882, 1928, 798, -332, -470, -608, -505, + -402, -139, -388, -589, -790, -991, -1192, -1794, -1884, -2006, -2128, -2266, -868, 818, 2504, 2288, 2072, 2112, + 2152, 2152, 2152, 2152, 1238, 1264, 1290, 1333, 1375, 1301, 1484, 2002, 2009, 1974, 1939, 1872, 1805, 1608, 1411, + 1118, 826, 751, 676, 505, 334, 273, 212, 151, 91, 69, 48, 10, -27, 482, 1758, 1771, 1784, 2033, 1771, 1860, 1950, + 1989, 2029, 884, -260, -1157, -261, -310, -614, -923, -975, -1412, -1848, -2062, -2019, -697, 626, 2060, 2471, + 2273, 2076, 2051, 2026, 2081, 2136, 2136, 2136, 2136, 1228, 1245, 1263, 1313, 1363, 1260, 1670, 2080, 1978, 1947, + 1916, 1853, 1791, 1580, 1369, 1094, 820, 718, 616, 450, 285, 243, 201, 159, 118, 69, 20, 3, -13, 910, 1833, 1828, + 1824, 229, 1706, 1839, 1972, 1901, 1830, 1983, 2136, 2032, 1416, 1056, 696, 280, 376, 728, 1080, 1767, 2454, 2405, + 2356, 2035, 2226, 2193, 2160, 2070, 1980, 2050, 2120, 2120, 2120, 2120, 1218, 1226, 1236, 1293, 1350, 1235, 1888, + 2061, 1979, 1936, 1893, 1834, 1776, 1551, 1327, 1070, 814, 685, 556, 395, 235, 212, 190, 167, 145, 116, 88, -68, + 32, 1306, 1812, 1949, 1576, -200, -183, 905, 1994, 1956, 1919, 1881, 1844, 2004, 1909, 2005, 2102, 2042, 2239, + 2195, 2152, 2043, 1935, 2370, 2038, 2697, 1821, 368, 2244, 2121, 1998, 2051, 2104, 2104, 2104, 2104, 1208, 1208, + 1209, 1273, 1338, 1210, 2107, 2043, 1980, 1925, 1871, 1816, 1762, 1523, 1285, 1046, 808, 652, 497, 341, 186, 182, + 179, 175, 172, 164, 157, 117, 590, 1958, 1791, 1815, 816, 140, -24, -28, -32, 988, 2008, 2036, 2064, 1977, 1890, + 1931, 1972, 2013, 2054, 2127, 2200, 2320, 2440, 2080, 184, -1760, -3192, 336, 2328, 2172, 2016, 2052, 2088, 2088, + 2088, 2088, 1222, 1215, 1209, 1267, 1325, 1459, 2105, 2046, 1989, 1946, 1904, 1861, 1819, 1612, 1406, 1136, 866, + 715, 565, 446, 328, 295, 263, 231, 199, 481, 765, 712, 1427, 2086, 1721, 1692, 128, -37, 55, -14, -82, -109, -135, + 334, 804, 1293, 1783, 2272, 2250, 2197, 1889, 1356, 568, -764, -2095, -3011, -2646, -2932, -2705, 2305, 2196, 2159, + 2122, 2117, 2112, 2112, 2112, 2112, 1236, 1223, 1210, 1261, 1313, 1708, 2103, 2050, 1998, 1967, 1937, 1907, 1877, + 1702, 1528, 1226, 924, 778, 633, 552, 471, 409, 348, 287, 226, 287, 349, 283, 1241, 1702, 1652, 1826, -48, 43, 134, + 1, -132, -181, -230, -343, -456, -670, -884, -202, -544, -946, -1860, -1718, -2088, -2311, -2534, -2469, -2404, + -2311, -1706, 2483, 2064, 2146, 2228, 2182, 2136, 2136, 2136, 2136, 1250, 1230, 1211, 1255, 1300, 1957, 2101, 2054, + 2007, 1956, 1906, 1856, 1806, 1696, 1586, 1284, 982, 841, 701, 657, 613, 555, 497, 439, 381, 413, 445, 718, 1758, + 1782, 1807, 1095, -128, -70, -11, -97, -182, -254, -325, -429, -532, -762, -991, -581, -170, -1034, -873, -1977, + -1800, -2019, -2237, -2344, -2450, -2651, -35, 2308, 2092, 2117, 2142, 2151, 2160, 2160, 2160, 2160, 1264, 1238, + 1212, 1250, 1288, 2206, 2100, 2058, 2016, 1946, 1876, 1806, 1736, 1690, 1644, 1342, 1040, 905, 770, 763, 756, 701, + 646, 591, 536, 539, 542, 897, 1764, 1607, 1962, 365, -208, -182, -156, -194, -232, -326, -420, -514, -608, -853, + -1098, -1471, -820, -97, -910, -955, -2024, -2238, -2452, -2474, -2496, -2990, 1636, 2134, 2120, 2088, 2056, 2120, + 2184, 2184, 2184, 2184, 1198, 1191, 1185, 1227, 1525, 2065, 2093, 2009, 1925, 1887, 1850, 1781, 1712, 1682, 1653, + 1464, 1275, 1130, 986, 937, 889, 841, 792, 744, 696, 685, 674, 1335, 1741, 1840, 1939, 54, -294, -296, -298, -299, + -301, -415, -528, -642, -755, -948, -1140, -1733, -1813, -734, -166, -1039, -887, -1235, -1582, -1610, -1637, + -1158, 2392, 2279, 2166, 2119, 2072, 2121, 2170, 2170, 2170, 2170, 1132, 1145, 1159, 1205, 1763, 1924, 2086, 1960, + 1834, 1829, 1825, 1756, 1688, 1675, 1663, 1586, 1510, 1356, 1202, 1112, 1023, 981, 939, 897, 856, 831, 807, 1774, + 1718, 1817, 1405, -512, -380, -410, -439, -404, -369, -503, -636, -769, -902, -1042, -1182, -1482, -1782, -2138, + -1982, -610, -262, -487, -712, -745, -777, 162, 2125, 1912, 2212, 2150, 2088, 2122, 2156, 2156, 2156, 2156, 1194, + 1147, 1101, 1182, 1776, 1927, 2079, 1863, 1903, 1979, 1799, 1843, 1632, 1620, 1608, 1612, 1617, 1517, 1418, 1351, + 1284, 1217, 1150, 1098, 1048, 945, 1099, 1781, 1695, 1954, 422, -566, -530, -556, -580, -573, -566, -687, -808, + -929, -1049, -1233, -1416, -1680, -1943, -2343, -2486, -2502, -2773, -2076, -1378, -1672, -2222, 458, 2370, 2137, + 2162, 2133, 2104, 2123, 2142, 2142, 2142, 2142, 1256, 1149, 1043, 1160, 1790, 1931, 2073, 1766, 1972, 2129, 1774, + 1931, 1576, 1565, 1554, 1639, 1724, 1679, 1635, 1590, 1546, 1453, 1361, 1300, 1240, 1060, 1392, 1788, 1672, 2092, + -560, -620, -680, -701, -721, -742, -762, -871, -979, -1088, -1196, -1423, -1650, -1877, -2104, -2291, -2478, + -2857, -2724, -2896, -3067, -3111, -3666, 2546, 2103, 2107, 2112, 2116, 2120, 2124, 2128, 2128, 2128, 2128, 1214, + 1171, 1128, 1453, 1779, 1692, 1862, 1807, 1753, 1732, 1712, 1804, 1640, 1759, 1623, 1711, 1799, 1666, 1791, 1755, + 1719, 1629, 1539, 1497, 1456, 1352, 1504, 1752, 1745, 1445, -903, -899, -894, -909, -923, -937, -951, -1071, -1191, + -1312, -1431, -1642, -1852, -2063, -2273, -2432, -2590, -2813, -2779, -2931, -3081, -3280, -2199, 2298, 2187, 2124, + 2062, 2081, 2100, 2119, 2138, 2138, 2138, 2138, 1172, 1193, 1214, 1747, 1769, 1710, 2163, 2360, 2046, 1592, 1651, + 1677, 1704, 1954, 1693, 1783, 1874, 1654, 1947, 1920, 1893, 1805, 1718, 1695, 1672, 1644, 1617, 1717, 1818, 798, + -1245, -1177, -1108, -1116, -1124, -1132, -1139, -1271, -1403, -1535, -1666, -1860, -2054, -2248, -2442, -2572, + -2702, -2768, -2834, -2965, -3095, -3193, -219, 2306, 2272, 2142, 2012, 2046, 2080, 2114, 2148, 2148, 2148, 2148, + 1194, 1151, 1364, 1785, 1694, 1983, 2272, 1441, 2147, 1980, 1813, 1838, 1864, 1909, 1698, 1823, 1949, 1818, 1943, + 1989, 2034, 1933, 1833, 1812, 1792, 1712, 1633, 1650, 1923, -537, -1460, -1391, -1322, -1356, -1389, -1423, -1456, + -1567, -1679, -1790, -1901, -2079, -2256, -2434, -2611, -2745, -2878, -2916, -2953, -2999, -3045, -3778, 1632, + 2298, 1941, 2015, 2090, 2107, 2124, 2141, 2158, 2158, 2158, 2158, 1216, 1109, 1514, 1823, 1620, 2001, 1870, 1803, + 1224, 1600, 1464, 1232, 1000, 1096, 1192, 1352, 1512, 1726, 1940, 2058, 2176, 2062, 1948, 1930, 1912, 1781, 1650, + 1583, 2028, -1871, -1674, -1605, -1536, -1595, -1654, -1713, -1772, -1863, -1954, -2045, -2136, -2297, -2458, + -2619, -2780, -2917, -3054, -3063, -3072, -3033, -2994, -2827, 2460, 2035, 2122, 2145, 2168, 2168, 2168, 2168, + 2168, 2168, 2168, 2168, 1190, 1272, 1610, 1757, 1647, 1523, 1144, 1324, 1249, 1364, 1224, 1211, 1199, 1255, 1566, + 1430, 1294, 1404, 1514, 1800, 2087, 2075, 2063, 2003, 1944, 1654, 1621, 1812, 979, -1999, -1904, -1889, -1874, + -1929, -1983, -2038, -2092, -2164, -2237, -2309, -2381, -2514, -2646, -2779, -2911, -3006, -3100, -3115, -3129, + -3040, -3206, -1085, 2317, 2104, 2148, 2159, 2171, 2175, 2179, 2183, 2187, 2187, 2187, 2187, 1164, 1179, 1195, + 1179, 1163, 1302, 1442, 1358, 1274, 1385, 1496, 1447, 1399, 1158, 1429, 1508, 1588, 1594, 1601, 1543, 1486, 1832, + 2179, 2077, 1976, 1528, 1593, 1785, -582, -2382, -2133, -2173, -2212, -2262, -2312, -2362, -2411, -2465, -2519, + -2573, -2626, -2730, -2834, -2938, -3042, -3094, -3146, -3166, -3186, -3046, -3418, 658, 2174, 2174, 2174, 2174, + 2174, 2182, 2190, 2198, 2206, 2206, 2206, 2206, 1202, 1230, 1260, 1273, 1286, 1321, 1356, 1343, 1331, 1405, 1480, + 1475, 1470, 1349, 1484, 1522, 1562, 1576, 1591, 1574, 1557, 1589, 1622, 1719, 1816, 1690, 1820, 1694, -2015, -2557, + -2331, -2377, -2422, -2612, -2801, -2702, -2603, -2670, -2737, -2805, -2871, -2947, -3022, -3098, -3173, -3183, + -3192, -3154, -3115, -3325, -3278, 2256, 2159, 2147, 2136, 2156, 2177, 2189, 2201, 2213, 2225, 2225, 2225, 2225, + 1240, 1282, 1325, 1367, 1410, 1340, 1271, 1329, 1388, 1426, 1465, 1503, 1542, 1540, 1539, 1537, 1536, 1559, 1582, + 1605, 1628, 1603, 1578, 1617, 1656, 1596, 1536, 1604, -2936, -2476, -2528, -2580, -2632, -2705, -2777, -2786, + -2794, -2875, -2955, -3036, -3116, -3163, -3210, -3257, -3304, -3271, -3238, -3141, -3044, -3091, -2114, 2319, + 2144, 2121, 2098, 2139, 2180, 2196, 2212, 2228, 2244, 2244, 2244, 2244, 1230, 1255, 1281, 1307, 1333, 1303, 1273, + 1338, 1405, 1436, 1469, 1501, 1533, 1535, 1537, 1539, 1542, 1563, 1584, 1605, 1627, 1602, 1577, 1616, 1656, 1807, + 1959, -417, -2793, -2798, -2546, -2582, -2618, -2689, -2758, -2796, -2834, -2902, -2970, -3038, -3105, -3146, + -3186, -3179, -3171, -3150, -3128, -3059, -2989, -3222, -126, 2281, 2129, 2084, 2040, 2107, 2175, 2189, 2203, 2217, + 2231, 2231, 2231, 2231, 1220, 1229, 1238, 1247, 1257, 1266, 1275, 1348, 1422, 1447, 1473, 1499, 1525, 1530, 1536, + 1542, 1548, 1567, 1587, 1606, 1626, 1601, 1577, 1616, 1656, 1763, 1871, 1658, -2138, -2863, -2563, -2584, -2604, + -2672, -2739, -2806, -2873, -2929, -2984, -3039, -3094, -3128, -3162, -3100, -3038, -3028, -3018, -2976, -2934, + -3352, 1862, 2244, 2114, 2048, 1982, 2076, 2170, 2182, 2194, 2206, 2218, 2218, 2218, 2218, 1210, 1234, 1259, 1283, + 1308, 1325, 1341, 1390, 1439, 1458, 1477, 1497, 1516, 1525, 1535, 1544, 1554, 1571, 1589, 1607, 1625, 1616, 1608, + 1632, 1656, 1719, 1782, 1685, 1845, 528, -2837, -2730, -2622, -2655, -2688, -2720, -2753, -2764, -2774, -2993, + -2955, -3031, -3106, -2814, -2777, -3227, -2908, -3134, -3359, -971, 2186, 2270, 2099, 2075, 2052, 2108, 2165, + 2175, 2185, 2195, 2205, 2205, 2205, 2205, 1200, 1240, 1280, 1320, 1360, 1384, 1408, 1432, 1456, 1469, 1482, 1495, + 1508, 1521, 1534, 1547, 1560, 1576, 1592, 1608, 1624, 1632, 1640, 1648, 1656, 1675, 1694, 1713, 1732, 1871, 986, + -827, -2640, -2638, -2636, -2634, -2632, -2598, -2564, -2946, -2816, -2933, -3050, -2783, -3028, -3169, -1774, 293, + 2360, 2179, 1998, 2041, 2084, 2103, 2122, 2141, 2160, 2168, 2176, 2184, 2192, 2192, 2192, 2192, 1232, 1266, 1300, + 1334, 1368, 1390, 1412, 1434, 1456, 1469, 1482, 1495, 1508, 1521, 1534, 1547, 1560, 1578, 1596, 1614, 1632, 1640, + 1648, 1656, 1664, 1646, 1628, 1706, 1784, 2102, 1908, 1298, 688, 1071, -594, -1587, -2580, -2891, -3202, -2281, + -2640, -2058, -1476, -94, 1032, 2278, 2244, 2210, 2176, 2132, 2088, 2092, 2096, 2112, 2128, 2144, 2160, 2168, 2176, + 2184, 2192, 2192, 2192, 2192, 1264, 1292, 1320, 1348, 1376, 1396, 1416, 1436, 1456, 1469, 1482, 1495, 1508, 1521, + 1534, 1547, 1560, 1580, 1600, 1620, 1640, 1648, 1656, 1664, 1672, 1617, 1562, 1699, 1836, 1821, 1806, 1887, 1968, + 1964, 1960, 2020, 2080, 1936, 1792, 1200, 1632, 1889, 2146, 2083, 2020, 2093, 2166, 2079, 1992, 2085, 2178, 2143, + 2108, 2121, 2134, 2147, 2160, 2168, 2176, 2184, 2192, 2192, 2192, 2192, 1296, 1318, 1340, 1362, 1384, 1402, 1420, + 1438, 1456, 1469, 1482, 1495, 1508, 1521, 1534, 1547, 1560, 1582, 1604, 1626, 1648, 1656, 1664, 1672, 1680, 1668, + 1656, 1740, 1824, 1812, 1800, 1836, 1872, 1881, 1890, 1819, 1748, 1995, 450, 937, 912, 716, 2056, 2020, 1984, 2036, + 2088, 2060, 2032, 2086, 2140, 2130, 2120, 2130, 2140, 2150, 2160, 2168, 2176, 2184, 2192, 2192, 2192, 2192, 1328, + 1344, 1360, 1376, 1392, 1408, 1424, 1440, 1456, 1469, 1482, 1495, 1508, 1521, 1534, 1547, 1560, 1584, 1608, 1632, + 1656, 1664, 1672, 1680, 1688, 1719, 1750, 1781, 1812, 1803, 1794, 1785, 1776, 1798, 1820, 1874, 1928, 1798, 2180, + 674, 1216, 2103, 1966, 1957, 1948, 1979, 2010, 2041, 2072, 2087, 2102, 2117, 2132, 2139, 2146, 2153, 2160, 2168, + 2176, 2184, 2192, 2192, 2192, 2192, 1328, 1344, 1360, 1376, 1392, 1408, 1424, 1440, 1456, 1469, 1482, 1495, 1508, + 1521, 1534, 1547, 1560, 1584, 1608, 1632, 1656, 1664, 1672, 1680, 1688, 1719, 1750, 1781, 1812, 1803, 1794, 1785, + 1776, 1798, 1820, 1858, 1896, 1750, 1860, 2338, 1792, 2135, 1966, 1957, 1948, 1979, 2010, 2041, 2072, 2087, 2102, + 2117, 2132, 2139, 2146, 2153, 2160, 2168, 2176, 2184, 2192, 2192, 2192, 2192, 1328, 1344, 1360, 1376, 1392, 1408, + 1424, 1440, 1456, 1469, 1482, 1495, 1508, 1521, 1534, 1547, 1560, 1584, 1608, 1632, 1656, 1664, 1672, 1680, 1688, + 1719, 1750, 1781, 1812, 1803, 1794, 1785, 1776, 1798, 1820, 1842, 1864, 1958, 2052, 1954, 1856, 1911, 1966, 1957, + 1948, 1979, 2010, 2041, 2072, 2087, 2102, 2117, 2132, 2139, 2146, 2153, 2160, 2168, 2176, 2184, 2192, 2192, 2192, + 2192, 1328, 1344, 1360, 1376, 1392, 1408, 1424, 1440, 1456, 1469, 1482, 1495, 1508, 1521, 1534, 1547, 1560, 1584, + 1608, 1632, 1656, 1664, 1672, 1680, 1688, 1719, 1750, 1781, 1812, 1803, 1794, 1785, 1776, 1798, 1820, 1842, 1864, + 1958, 2052, 1954, 1856, 1911, 1966, 1957, 1948, 1979, 2010, 2041, 2072, 2087, 2102, 2117, 2132, 2139, 2146, 2153, + 2160, 2168, 2176, 2184, 2192, 2192, 2192, 2192, +]; + +// 4.2.4.3.6 11.5 +const YCBCR_BUFFER_CR: [i16; 4096] = [ + -2112, -2114, -2116, -2118, -2120, -2122, -2124, -2126, -2128, -2118, -2108, -2098, -2088, -2150, -2212, -2146, + -2080, -2100, -2120, -2140, -2160, -2164, -2168, -2172, -2176, -2092, -2008, -2052, -2096, -2132, -2168, -2076, + -1984, -2088, -2192, -2168, -2144, -2136, -2128, -2120, -2112, -2126, -2140, -2154, -2168, -2150, -2132, -2114, + -2096, -2096, -2096, -2096, -2096, -2096, -2096, -2096, -2096, -2080, -2064, -2048, -2032, -2032, -2032, -2032, + -2128, -2114, -2099, -2116, -2133, -2134, -2135, -2136, -2137, -2128, -2118, -2108, -2098, -2118, -2138, -2126, + -2114, -2135, -2155, -2160, -2164, -2136, -2109, -2129, -2149, -2133, -2117, -2116, -2116, -2116, -2115, -2099, + -2082, -2113, -2143, -2142, -2140, -2134, -2129, -2123, -2117, -2128, -2138, -2148, -2158, -2147, -2135, -2123, + -2111, -2109, -2107, -2105, -2102, -2102, -2102, -2102, -2101, -2087, -2073, -2059, -2045, -2045, -2045, -2045, + -2144, -2113, -2081, -2113, -2145, -2146, -2146, -2146, -2146, -2137, -2127, -2117, -2107, -2086, -2064, -2106, + -2148, -2169, -2190, -2179, -2167, -2108, -2049, -2086, -2122, -2174, -2225, -2180, -2135, -2099, -2062, -2121, + -2180, -2137, -2094, -2115, -2135, -2132, -2129, -2126, -2122, -2129, -2135, -2142, -2148, -2143, -2137, -2132, + -2126, -2122, -2117, -2113, -2108, -2108, -2107, -2107, -2106, -2094, -2082, -2070, -2058, -2058, -2058, -2058, + -2160, -2112, -2063, -2111, -2158, -2158, -2157, -2156, -2155, -2146, -2136, -2127, -2117, -2134, -2150, -2134, + -2118, -2156, -2193, -2182, -2171, -2496, -2309, -2395, -2479, -2471, -2461, -2244, -2283, -2354, -2169, -2176, + -2182, -2162, -2141, -2136, -2131, -2130, -2129, -2129, -2127, -2130, -2133, -2136, -2138, -2139, -2140, -2141, + -2141, -2135, -2128, -2121, -2114, -2114, -2113, -2112, -2111, -2101, -2091, -2081, -2071, -2071, -2071, -2071, + -2176, -2111, -2045, -2108, -2170, -2169, -2167, -2166, -2164, -2155, -2145, -2136, -2126, -2181, -2235, -2162, + -2088, -2142, -2195, -2441, -2686, -2372, -1033, -399, 236, 305, 375, -4, -894, -2097, -2787, -2486, -2184, -2186, + -2187, -2157, -2126, -2128, -2129, -2131, -2132, -2131, -2130, -2129, -2128, -2135, -2142, -2149, -2156, -2147, + -2138, -2129, -2120, -2119, -2118, -2117, -2116, -2108, -2100, -2092, -2084, -2084, -2084, -2084, -2112, -2086, + -2060, -2114, -2167, -2069, -2226, -2192, -2157, -2108, -2059, -2106, -2152, -2121, -2089, -2634, -2666, -2265, + -838, 844, 2526, 3326, 2846, 2846, 2847, 2726, 2606, 2966, 3070, 2968, 2866, 395, -2074, -2747, -2138, -2282, + -2170, -2204, -2238, -2192, -2145, -2147, -2148, -2149, -2150, -2154, -2157, -2160, -2163, -2160, -2157, -2154, + -2150, -2131, -2112, -2125, -2137, -2127, -2117, -2107, -2097, -2097, -2097, -2097, -2048, -2061, -2074, -2119, + -2163, -1968, -2285, -2218, -2150, -2061, -1972, -2075, -2177, -2316, -2455, -1058, 1364, 2989, 2567, 2593, 2619, + 2368, 2630, 2508, 2386, 2332, 2278, 2352, 2427, 2913, 2887, 3021, 3156, 1301, -2089, -2407, -2213, -2280, -2346, + -2252, -2158, -2162, -2165, -2169, -2172, -2172, -2171, -2171, -2170, -2173, -2175, -2178, -2180, -2143, -2105, + -2132, -2158, -2146, -2134, -2122, -2110, -2110, -2110, -2110, -2112, -2164, -2216, -2236, -2256, -1996, -2248, + -2196, -2143, -2110, -2077, -2124, -2171, -2272, 699, 3526, 2770, 2035, 2324, 2294, 2263, 2178, 2350, 2265, 2181, + 2129, 2078, 2154, 2231, 2522, 2556, 2559, 2562, 3221, 3112, 140, -2833, -2036, -2262, -2201, -2139, -2161, -2183, + -2189, -2194, -2190, -2186, -2182, -2177, -2186, -2194, -2202, -2210, -2155, -2099, -2139, -2179, -2165, -2151, + -2137, -2123, -2123, -2123, -2123, -1664, -1755, -1846, -1841, -1836, -1767, -2210, -2173, -2136, -2159, -2182, + -2173, -2164, -2739, 2830, 2735, 2640, 2361, 2082, 1995, 1908, 1989, 2070, 2023, 1976, 1927, 1878, 1957, 2036, + 2131, 2226, 2353, 2480, 2581, 2682, 2943, 2692, -2815, -2178, -2149, -2120, -2160, -2200, -2208, -2216, -2208, + -2200, -2192, -2184, -2198, -2212, -2226, -2240, -2166, -2092, -2146, -2200, -2184, -2168, -2152, -2136, -2136, + -2136, -2136, -2096, -2168, -2239, -2230, -2221, -2088, -2211, -2174, -2137, -2191, -2244, -2153, -2318, -2032, + 3375, 2862, 2605, 2306, 2007, 1852, 1697, 1756, 1815, 1810, 1806, 1756, 1707, 1754, 1801, 1912, 2023, 2150, 2277, + 2300, 2323, 2730, 1345, -2440, -2129, -2218, -2307, -2350, -2137, -2180, -2223, -2224, -2225, -2194, -2162, -2172, + -2181, -2191, -2200, -2199, -2199, -2214, -2229, -2172, -2115, -2170, -2225, -2113, -2257, -2257, -2016, -2068, + -2119, -2106, -2093, -2153, -2212, -2175, -2138, -2222, -2305, -2133, -2472, 212, 2897, 2477, 2570, 2251, 1932, + 1709, 1487, 1524, 1561, 1598, 1636, 1586, 1537, 1552, 1567, 1693, 1820, 1947, 2074, 2019, 1964, 2261, -514, -2321, + -2080, -2031, -1982, -2284, -2074, -2152, -2229, -2239, -2249, -2195, -2140, -2145, -2150, -2155, -2159, -2232, + -2305, -2282, -2258, -2160, -2062, -2188, -2314, -2090, -2378, -2378, -2064, -2096, -2127, -2127, -2126, -2154, + -2181, -2160, -2139, -2205, -2271, -2145, -2530, 1688, 2834, 2460, 2343, 2148, 1953, 1678, 1404, 1387, 1371, 1418, + 1466, 1416, 1367, 1349, 1332, 1442, 1553, 1664, 1775, 1818, 1861, 2416, -2405, -2458, -1999, -2036, -281, -1466, + -2395, -2380, -2364, -2303, -2241, -2196, -2150, -2167, -2183, -2183, -2183, -2201, -2219, -2190, -2159, -2756, + -2329, -1934, -2307, -2627, -2179, -2307, -2112, -2124, -2135, -2147, -2158, -2154, -2149, -2145, -2140, -2188, + -2236, -2156, -2588, 3164, 2772, 2444, 2116, 2045, 1975, 1648, 1322, 1251, 1181, 1238, 1296, 1246, 1197, 1147, + 1098, 1192, 1287, 1381, 1476, 1617, 1758, 1291, -2760, -2083, -2430, -1273, -628, -648, -667, -1583, -2498, -2366, + -2233, -2197, -2160, -2188, -2215, -2211, -2206, -2170, -2133, -2097, -2060, -280, -548, -2448, -1788, -860, -1980, + -2236, -2112, -2122, -2132, -2142, -2151, -2147, -2142, -2138, -2133, -2148, -2162, -2080, -718, 3207, 2525, 2291, + 2057, 1942, 1828, 1553, 1279, 1174, 1070, 1094, 1118, 1044, 970, 976, 983, 1001, 1020, 1166, 1313, 1306, 1555, + -212, -2491, -2190, -2401, -868, -615, -644, -672, -605, -537, -1356, -2174, -2272, -2370, -2342, -2312, -2331, + -2350, -2317, -2284, -2699, -1321, -420, -543, -394, -757, -741, -2261, -2261, -2112, -2120, -2128, -2136, -2143, + -2139, -2135, -2131, -2126, -2107, -2087, -2260, 640, 2995, 2279, 2138, 1998, 1839, 1681, 1459, 1237, 1098, 960, + 950, 940, 842, 744, 806, 869, 811, 753, 951, 1150, 995, 1352, -1715, -2222, -2297, -2372, -463, -602, -640, -677, + -650, -623, -601, -578, -811, -1044, -1215, -1385, -1427, -1469, -1184, -898, -484, -582, -560, -538, -900, -750, + -1134, -2542, -2286, -2112, -2118, -2124, -2130, -2136, -2132, -2128, -2124, -2119, -2018, -1917, -2888, 1262, + 2015, 2256, 2097, 1939, 1736, 1534, 1364, 1194, 1022, 850, 806, 762, 736, 710, 508, 818, 604, 646, 752, 859, 1132, + 1149, -2866, -2273, -2340, -1639, -426, -493, -524, -554, -568, -582, -678, -774, -662, -550, -568, -586, -587, + -589, -659, -728, -574, -675, -668, -661, -798, -679, -1799, -2407, -2151, -2112, -2116, -2120, -2124, -2128, + -2124, -2120, -2116, -2112, -2185, -2258, -1723, 1884, 1035, 2234, 2057, 1880, 1634, 1388, 1270, 1152, 946, 740, + 662, 584, 630, 676, 466, 1280, 654, 540, 554, 568, 757, -78, -2481, -2324, -2383, -906, -389, -384, -407, -430, + -485, -540, -499, -458, -513, -568, -689, -810, -771, -732, -645, -558, -663, -768, -776, -784, -696, -608, -2464, + -2272, -2016, -2104, -2111, -2117, -2123, -2129, -2106, -2082, -2106, -2130, -2206, -2537, -85, 1856, 1149, 1209, + 1702, 1683, 1507, 1332, 1188, 1045, 837, 630, 518, 407, 489, 572, 398, 1249, 662, 330, 383, 436, 589, -1305, -2352, + -2118, -2616, 213, -13, -239, -267, -294, -322, -349, -378, -408, -485, -562, -627, -692, -677, -661, -626, -591, + -684, -776, -804, -832, -540, -248, -664, -1848, -2616, -2096, -2105, -2113, -2122, -2130, -2087, -2043, -2096, + -2148, -2226, -2816, 1554, 1829, 1519, 697, 1603, 1486, 1381, 1276, 1107, 938, 729, 520, 375, 230, 349, 468, 331, + 1219, 670, 121, 212, 304, 422, -2532, -2478, -2423, -1569, 309, -149, -94, -126, -158, -158, -157, -257, -357, + -457, -556, -565, -573, -582, -590, -607, -623, -704, -784, -832, -880, -384, 112, -1424, -2448, -2192, -2088, + -2099, -2110, -2121, -2131, -2100, -2069, -2102, -2134, -2487, -2327, 2921, 2025, 1537, 1049, 1088, 1385, 1270, + 1156, 993, 831, 700, 570, 407, 245, 256, 268, 344, 932, 662, 136, 185, 236, -338, -2447, -2348, -2505, -794, 149, + -77, -45, -66, -86, -90, -94, -184, -274, -365, -454, -455, -455, -519, -583, -620, -656, -724, -792, -796, -800, + -868, -1960, -2296, -2376, -2248, -2080, -2093, -2106, -2119, -2132, -2113, -2094, -2107, -2120, -2235, -813, 2752, + 2222, 1555, 1401, 574, 1284, 1160, 1036, 880, 724, 672, 620, 440, 260, 164, 69, 357, 646, 654, 151, 159, 168, + -1097, -2361, -2218, -2586, -19, -11, -4, 4, -5, -13, -22, -30, -111, -191, -272, -352, -344, -336, -456, -576, + -632, -688, -744, -800, -760, -720, -584, -2496, -2400, -2304, -2304, -2072, -2088, -2103, -2118, -2133, -2173, + -2212, -2171, -2130, -2464, 1044, 2615, 2138, 1657, 1432, 807, 951, 1193, 924, 734, 545, 397, 250, 486, 723, 569, + 417, 312, 207, 384, 305, 242, 180, -1827, -2296, -2350, -1892, 68, -20, -12, -3, -9, -13, -18, -23, -66, -109, + -184, -258, -310, -362, -478, -593, -641, -689, -737, -784, -752, -720, -1200, -2448, -2384, -2320, -2320, -2064, + -2082, -2099, -2117, -2134, -2232, -2329, -2235, -2140, -2692, 2901, 2478, 2055, 1759, 1464, 1041, 618, 1227, 812, + 589, 366, 379, 392, 277, 162, 207, 253, 267, 281, 114, -52, 70, 192, -2556, -2231, -2482, -1197, 155, -28, -19, + -10, -12, -13, -14, -15, -21, -26, -95, -164, -276, -387, -499, -610, -650, -689, -729, -768, -744, -720, -1816, + -2400, -2368, -2336, -2336, -2056, -2076, -2096, -2116, -2135, -2179, -2223, -2139, -2310, -1320, 2742, 2293, 2099, + 1893, 1432, 1242, 541, 1036, 1020, 699, 379, 376, 374, 275, 177, 197, 217, 190, 162, 100, 39, 153, -756, -2421, + -2294, -2550, -503, 130, -4, -11, -17, -15, -13, -10, -8, -8, -7, -103, -198, -322, -445, -520, -595, -643, -690, + -721, -752, -768, -784, -2192, -2320, -2336, -2352, -2352, -2048, -2070, -2092, -2114, -2136, -2126, -2116, -2042, + -2480, 52, 2584, 2108, 2144, 2028, 1400, 1444, 464, 78, -308, -470, -632, -394, -156, 18, 192, 187, 182, 113, 44, + 87, 130, 237, -1704, -2286, -2356, -2618, 192, 106, 20, -2, -24, -18, -12, -6, 0, 6, 12, -110, -232, -367, -502, + -541, -580, -635, -690, -713, -736, -792, -848, -2568, -2240, -2304, -2368, -2368, -2046, -2069, -2092, -2114, + -2137, -2122, -2106, -2187, -2523, 1999, 2681, 2739, 1518, 116, -1542, -2640, -2457, -2466, -2475, -2467, -2460, + -2499, -2537, -2304, -2070, -995, 81, -76, 24, 35, 47, -150, -2394, -2423, -2451, -1807, 117, 85, 53, 21, -11, -11, + -11, -11, -11, -11, -11, -107, -203, -405, -606, -616, -625, -611, -596, -694, -791, -757, -1491, -2401, -2287, + -2303, -2319, -2319, -2044, -2068, -2091, -2114, -2137, -2117, -2096, -2075, -2054, 2922, 219, -1749, -2692, -2564, + -2435, -2115, -2306, -2194, -2081, -2160, -2239, -2299, -2358, -2321, -2284, -2432, -2580, -1544, 4, -16, -36, + -280, -2572, -2303, -2545, -995, 43, 64, 86, 44, 2, -4, -10, -16, -22, -28, -34, -104, -174, -186, -198, -178, + -158, -330, -502, -674, -846, -722, -2134, -2234, -2334, -2302, -2270, -2270, -2042, -2067, -2090, -2114, -2138, + -2160, -2182, -2156, -2129, -2459, -2532, -2605, -2166, -2220, -2273, -2294, -2315, -2002, -2199, -2221, -2243, + -2323, -2403, -2387, -2370, -2286, -2201, -2453, -2704, -1412, 137, -1403, -2174, -2503, -2831, 248, 0, 27, 55, 35, + 15, 3, -9, -21, -33, -45, -57, -101, -145, -176, -206, -221, -235, -178, -120, -415, -709, -191, -2489, -2547, + -2349, -2349, -2349, -2349, -2040, -2065, -2089, -2114, -2138, -2203, -2267, -2236, -2204, -2207, -2210, -2181, + -2152, -2131, -2110, -2217, -1812, -1553, -2317, -2026, -1734, -1579, -1423, -1940, -2456, -2395, -2334, -2081, + -2340, -2551, -2250, -2013, -2288, -2447, -2093, -44, -42, -9, 25, 26, 28, 10, -8, -26, -44, -62, -80, -98, -116, + -165, -214, -263, -312, -281, -250, -155, -60, -940, -1820, -2348, -2364, -2396, -2428, -2428, -2038, -2060, -2081, + -2102, -2123, -2124, -2125, -2287, -2191, -2066, -1941, -1912, -1882, -2233, -2328, -2151, -1717, -1487, -2024, + -1761, -1498, -1243, -988, -718, -446, -1227, -2007, -2724, -2160, -2331, -2245, -2176, -2362, -2339, -1036, 107, + -29, -20, -10, 15, 41, 19, -3, -25, -47, -89, -131, -141, -151, -209, -266, -356, -445, -459, -472, -406, -83, + -1135, -1163, -1895, -2371, -2387, -2403, -2403, -2036, -2054, -2072, -2090, -2107, -2045, -1983, -2081, -1666, + -1669, -1671, -1898, -2124, -2591, -2545, -2084, -1622, -1420, -1730, -1496, -1261, -1163, -1065, -775, -484, -314, + -144, -806, -2492, -2366, -2240, -2338, -2436, -2487, -490, 3, -15, -30, -45, 4, 54, 28, 2, -24, -50, -116, -182, + -184, -186, -252, -318, -448, -578, -636, -694, -656, -106, -2098, -2042, -2210, -2378, -2378, -2378, -2378, -2034, + -2049, -2063, -2078, -2092, -2094, -2097, -1651, -1461, -1688, -1914, -2156, -2398, -2677, -2443, -2017, -1591, + -1450, -1564, -1343, -1121, -987, -854, -624, -394, -266, -137, 199, 24, -1554, -2363, -2325, -2286, -2123, -2728, + -1221, 30, 135, -16, 25, 67, 37, 7, -7, -21, -111, -201, -211, -221, -296, -370, -461, -551, -510, -468, -635, + -545, -2805, -2249, -2301, -2353, -2353, -2353, -2353, -2032, -2043, -2054, -2065, -2076, -2143, -2210, -1477, + -1768, -1962, -2156, -2414, -2672, -2762, -2340, -1950, -1560, -1479, -1398, -1189, -980, -811, -642, -473, -304, + -217, -130, -75, -20, 27, -2486, -2311, -2136, -2527, -2406, -2445, -2484, -979, 14, 47, 80, 46, 12, 10, 8, -106, + -220, -238, -256, -339, -422, -473, -524, -639, -754, -1637, -2520, -2232, -2456, -2392, -2328, -2328, -2328, + -2328, -2012, -2031, -2050, -2053, -2056, -2193, -2074, -1587, -1867, -2082, -2296, -2527, -2757, -2654, -2294, + -1887, -1479, -1381, -1282, -1088, -893, -749, -604, -492, -379, -244, -109, -182, 1, -606, -2493, -2284, -2331, + -2482, -2376, -2415, -2453, -2309, -2422, -1350, -278, -125, 29, 87, 145, 126, 108, 26, -56, -279, -502, -1108, + -1715, -2164, -2612, -2533, -2453, -2297, -2397, -2369, -2341, -2341, -2341, -2341, -1992, -2019, -2046, -2041, + -2035, -2242, -1937, -1696, -1966, -2201, -2436, -2639, -2842, -2545, -2248, -1823, -1398, -1282, -1166, -986, + -806, -686, -566, -510, -454, -271, -88, -289, 22, -1239, -2500, -2257, -2526, -388, -2346, -2384, -2421, -2359, + -2297, -2491, -2684, -2343, -2001, -1628, -1254, -1177, -1099, -1502, -1904, -2267, -2629, -2511, -2393, -2408, + -2422, -2404, -2386, -2362, -2338, -2346, -2354, -2354, -2354, -2354, -1972, -2007, -2042, -2045, -2047, -2196, + -1832, -1837, -2097, -2337, -2576, -2736, -2895, -2565, -2234, -1840, -1445, -1280, -1114, -917, -719, -624, -528, + -529, -529, -426, -323, -60, -53, -2528, -2443, -2518, -2081, 169, -140, -1313, -2486, -2441, -2396, -2384, -2370, + -2401, -2432, -2511, -2589, -2560, -2531, -2502, -2472, -2431, -2388, -2490, -2336, -2940, -2008, -1332, -2447, + -2395, -2343, -2355, -2367, -2367, -2367, -2367, -1952, -1995, -2037, -2048, -2058, -2149, -1727, -1978, -2228, + -2472, -2716, -2832, -2948, -2584, -2220, -1856, -1492, -1277, -1062, -847, -632, -561, -490, -547, -604, -581, + -558, -343, -1152, -2281, -2386, -2523, -1124, -41, 19, 14, 10, -1243, -2495, -2532, -2568, -2459, -2350, -2369, + -2388, -2407, -2426, -2477, -2528, -2594, -2659, -2213, -1254, 368, 967, -1027, -2508, -2428, -2348, -2364, -2380, + -2380, -2380, -2380, -1948, -1997, -2045, -2062, -2078, -1959, -1839, -2071, -2303, -2546, -2788, -2919, -3049, + -2874, -2442, -2027, -1611, -1375, -1138, -966, -793, -733, -672, -708, -743, -848, -953, -2018, -2059, -2442, + -2313, -2328, -295, 98, -19, 23, 65, 25, -15, -631, -1246, -1796, -2345, -2510, -2675, -2541, -2406, -1887, -1368, + -468, 433, 438, 699, 1162, 857, -2697, -2409, -2413, -2417, -2389, -2361, -2361, -2361, -2361, -1944, -1999, -2053, + -2075, -2097, -1768, -1950, -2164, -2378, -2619, -2860, -3005, -3150, -3163, -2664, -2197, -1730, -1472, -1214, + -1084, -954, -904, -854, -868, -882, -859, -836, -877, -1942, -2091, -2240, -2389, 22, -18, -57, 32, 121, 13, -94, + -9, 76, 148, 221, 165, 110, 142, 175, 239, 304, 379, 454, 529, 605, 676, 235, -2574, -2310, -2398, -2486, -2414, + -2342, -2342, -2342, -2342, -1940, -2001, -2061, -2073, -2085, -1641, -1965, -2145, -2325, -2533, -2740, -2900, + -3059, -3053, -2790, -2320, -1849, -1570, -1290, -1203, -1115, -1076, -1036, -1029, -1021, -1078, -1135, -504, + -2689, -2396, -2359, -1554, 19, -6, -31, 25, 80, 33, -13, 36, 86, 124, 162, 136, 111, 137, 163, 237, 312, 393, 475, + 524, 574, 654, -803, -2467, -2339, -2383, -2427, -2375, -2323, -2323, -2323, -2323, -1936, -2002, -2068, -2070, + -2072, -1514, -1980, -2126, -2272, -2446, -2620, -2794, -2968, -2942, -2916, -2442, -1968, -1667, -1366, -1321, + -1276, -1247, -1218, -1189, -1160, -1041, -922, -1411, -2412, -2189, -2478, -719, 16, 6, -4, 18, 40, 54, 68, 82, + 96, 100, 104, 108, 112, 132, 152, 236, 320, 408, 496, 520, 544, 632, -1840, -2360, -2368, -2368, -2368, -2336, + -2304, -2304, -2304, -2304, -1898, -1921, -1944, -2111, -1766, -1551, -1848, -1985, -2122, -2319, -2516, -2665, + -2814, -3075, -3080, -2829, -2321, -2026, -1730, -1610, -1490, -1458, -1426, -1394, -1362, -1247, -1132, -1880, + -2373, -2534, -2694, 329, 25, 40, 55, 54, 54, 71, 88, 105, 123, 151, 180, 208, 237, 83, -70, 48, 167, 248, 329, + 346, 363, 732, -2739, -2578, -2416, -2395, -2374, -2353, -2332, -2332, -2332, -2332, -1860, -1840, -1820, -2152, + -1460, -1588, -1716, -1844, -1972, -2192, -2412, -2536, -2659, -2951, -2731, -2959, -2674, -2384, -2093, -1898, + -1703, -1669, -1634, -1599, -1564, -1453, -1341, -2349, -2333, -2366, -1886, -158, 34, 74, 115, 91, 68, 88, 109, + 129, 150, 203, 256, 309, 362, 291, 220, 117, 14, 88, 162, 172, 183, -703, -2613, -2283, -2464, -2422, -2380, -2370, + -2360, -2360, -2360, -2360, -2110, -1967, -1824, -1953, -1314, -1513, -1712, -1815, -1918, -2209, -2244, -2455, + -2409, -2604, -2542, -2753, -2707, -2694, -2680, -2411, -2141, -2056, -1970, -1868, -1766, -1723, -1678, -2370, + -2294, -2518, -950, -54, 75, 92, 110, 96, 82, 105, 129, 153, 177, 222, 268, 313, 359, 354, 350, 441, 533, 472, 411, + 414, 674, -1691, -2519, -2340, -2416, -2401, -2386, -2387, -2388, -2388, -2388, -2388, -1848, -1838, -1828, -1754, + -1168, -1438, -1708, -1786, -1864, -2226, -2075, -2373, -2158, -2256, -2353, -2547, -2740, -2748, -2755, -2667, + -2578, -2442, -2305, -2137, -1968, -1992, -2015, -2391, -2254, -2670, -13, 51, 116, 111, 106, 101, 96, 123, 150, + 177, 204, 242, 280, 318, 356, 418, 480, 510, 540, 600, 661, 657, 1166, -2678, -2425, -2397, -2368, -2380, -2392, + -2404, -2416, -2416, -2416, -2416, -1882, -1711, -1796, -1369, -1198, -1419, -1640, -1749, -1858, -1979, -1843, + -2060, -2020, -2115, -2209, -2367, -2525, -2480, -2691, -2838, -2984, -2761, -2537, -2394, -2250, -2196, -2141, + -2358, -2319, -2020, 71, 113, 157, 151, 145, 139, 134, 160, 186, 212, 239, 273, 308, 342, 377, 439, 502, 548, 595, + 632, 670, 931, 169, -2668, -2432, -2404, -2376, -2385, -2394, -2403, -2412, -2412, -2412, -2412, -1916, -1840, + -2276, -1240, -1228, -1400, -1572, -1712, -1852, -1732, -1611, -1746, -1881, -1973, -2064, -2187, -2310, -2212, + -2626, -2752, -2877, -2823, -2769, -2651, -2532, -2399, -2266, -2325, -2383, -1370, 155, 176, 198, 191, 185, 178, + 172, 197, 223, 248, 274, 305, 336, 367, 398, 461, 524, 587, 650, 664, 679, 1206, -827, -2657, -2438, -2411, -2384, + -2390, -2396, -2402, -2408, -2408, -2408, -2408, -1950, -1953, -1956, -1063, -1194, -1317, -1440, -1435, -1430, + -1501, -1315, -1433, -1551, -1639, -1727, -1799, -1871, -1928, -2241, -2410, -2579, -2598, -2617, -2732, -2846, + -2555, -2263, -2260, -2512, -528, 175, 207, 239, 231, 224, 217, 210, 234, 259, 284, 309, 336, 364, 391, 419, 482, + 546, 609, 673, 744, 816, 936, -2016, -2486, -2188, -2290, -2392, -2395, -2398, -2401, -2404, -2404, -2404, -2404, + -1984, -2066, -1636, -886, -1160, -1234, -1308, -1414, -1520, -2037, -2042, -1887, -1732, -1817, -1902, -1923, + -1944, -1900, -1856, -2068, -2280, -2372, -2464, -2556, -2648, -2454, -2260, -2194, -2640, 314, 196, 238, 280, 272, + 264, 256, 248, 272, 296, 320, 344, 368, 392, 416, 440, 504, 568, 632, 696, 825, 954, 923, -2692, -2315, -2450, + -2425, -2400, -2400, -2400, -2400, -2400, -2400, -2400, -2400, -2252, -1954, -1143, -1036, -1441, -1827, -2212, + -2245, -2278, -2222, -1909, -1916, -1923, -2002, -2337, -2096, -2111, -2172, -2232, -2132, -2032, -2144, -2256, + -2304, -2352, -2307, -2261, -2360, -1690, 442, 269, 305, 341, 333, 325, 317, 309, 329, 349, 369, 389, 415, 441, + 468, 494, 536, 579, 669, 760, 797, 1091, -248, -2610, -2407, -2459, -2432, -2404, -2400, -2396, -2392, -2388, + -2388, -2388, -2388, -2008, -2097, -1673, -1954, -2234, -2163, -2091, -2052, -2012, -2150, -2287, -2200, -2113, + -1931, -2260, -2013, -2278, -2187, -2095, -2195, -2295, -2172, -2048, -2052, -2056, -2159, -2262, -2525, -739, 570, + 343, 372, 402, 394, 386, 378, 370, 386, 402, 418, 434, 462, 491, 520, 549, 569, 590, 707, 824, 770, 1228, -1418, + -2528, -2498, -2468, -2438, -2408, -2400, -2392, -2384, -2376, -2376, -2376, -2376, -1988, -2192, -2140, -2152, + -2163, -2131, -2099, -2083, -2066, -2142, -2217, -2181, -2144, -2068, -2247, -2138, -2285, -2234, -2182, -2227, + -2271, -2328, -2384, -2168, -1952, -2252, -2551, -2466, 179, 394, 353, 407, 463, 455, 447, 423, 399, 523, 391, 547, + 447, 493, 541, 572, 603, 634, 665, 792, 920, 1094, 1269, -2765, -2446, -2430, -2413, -2413, -2412, -2400, -2388, + -2376, -2364, -2364, -2364, -2364, -1968, -2031, -2094, -2093, -2092, -2099, -2106, -2113, -2120, -2134, -2147, + -2161, -2174, -2204, -2233, -2263, -2292, -2281, -2269, -2258, -2246, -2227, -2207, -2284, -2360, -2344, -2327, + -2407, 586, -38, 363, 443, 524, 516, 508, 468, 428, 660, 380, 676, 460, 525, 591, 624, 658, 699, 741, 878, 1016, + 907, 286, -2575, -2364, -2361, -2358, -2387, -2416, -2400, -2384, -2368, -2352, -2352, -2352, -2352, -2020, -2073, + -2125, -2081, -2037, -2064, -2090, -2116, -2142, -2154, -2166, -2178, -2189, -2213, -2236, -2260, -2283, -2276, + -2269, -2262, -2254, -2251, -2247, -2292, -2336, -2339, -2340, -1206, -72, -16, 296, 496, 441, 469, 497, 381, 521, + 635, 493, 735, 465, 544, 624, 640, 656, 748, 840, 899, 960, 1115, -1033, -2494, -2418, -2379, -2339, -2380, -2420, + -2408, -2396, -2384, -2372, -2372, -2372, -2372, -2072, -2114, -2155, -2069, -1982, -2028, -2073, -2119, -2164, + -2174, -2184, -2194, -2203, -2221, -2239, -2257, -2274, -2271, -2268, -2265, -2261, -2274, -2287, -2300, -2312, + -2333, -2353, -2053, -729, 6, 230, 550, 358, 422, 486, 294, 614, 610, 606, 794, 470, 564, 658, 656, 655, 797, 939, + 921, 904, 1324, -2352, -2412, -2472, -2396, -2320, -2372, -2424, -2416, -2408, -2400, -2392, -2392, -2392, -2392, + -1996, -1931, -1866, -1961, -2055, -2088, -2121, -2154, -2186, -2194, -2202, -2210, -2218, -2230, -2242, -2254, + -2265, -2266, -2267, -2268, -2269, -2282, -2295, -2308, -2320, -2343, -2366, -2708, -2539, -1492, -188, 171, 275, + 327, 379, 287, 451, 505, 559, 773, 475, 551, 628, 512, 653, 910, 654, 1007, 1104, -740, -2583, -2507, -2430, -2398, + -2365, -2397, -2428, -2424, -2420, -2416, -2412, -2412, -2412, -2412, -1920, -2004, -2088, -2108, -2128, -2148, + -2168, -2188, -2208, -2214, -2220, -2226, -2232, -2238, -2244, -2250, -2256, -2261, -2266, -2271, -2276, -2289, + -2302, -2315, -2328, -2353, -2378, -2339, -2300, -2477, -1630, -719, 192, 232, 272, 280, 288, 400, 512, 752, 480, + 539, 598, 369, 652, 767, -142, -1211, -2792, -2547, -2302, -2345, -2388, -2399, -2410, -2421, -2432, -2432, -2432, + -2432, -2432, -2432, -2432, -2432, -2024, -2070, -2116, -2130, -2144, -2164, -2184, -2204, -2224, -2228, -2232, + -2236, -2240, -2244, -2248, -2252, -2256, -2263, -2270, -2277, -2284, -2297, -2310, -2323, -2336, -2320, -2304, + -2288, -2272, -2560, -2336, -1856, -1376, -2264, -1104, -520, 64, 384, 704, 704, 192, -44, -280, -1236, -1936, + -3018, -2564, -2350, -2392, -2391, -2390, -2389, -2388, -2399, -2410, -2421, -2432, -2432, -2432, -2432, -2432, + -2432, -2432, -2432, -2128, -2136, -2144, -2152, -2160, -2180, -2200, -2220, -2240, -2242, -2244, -2246, -2248, + -2250, -2252, -2254, -2256, -2265, -2274, -2283, -2292, -2305, -2318, -2331, -2344, -2287, -2230, -2237, -2244, + -2387, -2530, -2481, -2432, -2456, -2480, -2600, -2720, -2448, -2176, -1904, -2144, -2419, -2694, -2585, -2476, + -2451, -2426, -2465, -2504, -2491, -2478, -2433, -2388, -2399, -2410, -2421, -2432, -2432, -2432, -2432, -2432, + -2432, -2432, -2432, -2104, -2122, -2140, -2158, -2176, -2196, -2216, -2236, -2256, -2256, -2256, -2256, -2256, + -2256, -2256, -2256, -2256, -2267, -2278, -2289, -2300, -2313, -2326, -2339, -2352, -2318, -2284, -2282, -2280, + -2358, -2436, -2418, -2400, -2408, -2416, -2360, -2304, -2480, -864, -1648, -1408, -1226, -2580, -2510, -2440, + -2428, -2416, -2436, -2456, -2447, -2438, -2413, -2388, -2399, -2410, -2421, -2432, -2432, -2432, -2432, -2432, + -2432, -2432, -2432, -2080, -2108, -2136, -2164, -2192, -2212, -2232, -2252, -2272, -2270, -2268, -2266, -2264, + -2262, -2260, -2258, -2256, -2269, -2282, -2295, -2308, -2321, -2334, -2347, -2360, -2349, -2338, -2327, -2316, + -2329, -2342, -2355, -2368, -2360, -2352, -2376, -2400, -2256, -2624, -1392, -1696, -2593, -2466, -2435, -2404, + -2405, -2406, -2407, -2408, -2403, -2398, -2393, -2388, -2399, -2410, -2421, -2432, -2432, -2432, -2432, -2432, + -2432, -2432, -2432, -2080, -2108, -2136, -2164, -2192, -2212, -2232, -2252, -2272, -2270, -2268, -2266, -2264, + -2262, -2260, -2258, -2256, -2269, -2282, -2295, -2308, -2321, -2334, -2347, -2360, -2349, -2338, -2327, -2316, + -2329, -2342, -2355, -2368, -2360, -2352, -2360, -2368, -2352, -2592, -2192, -2560, -2769, -2466, -2435, -2404, + -2405, -2406, -2407, -2408, -2403, -2398, -2393, -2388, -2399, -2410, -2421, -2432, -2432, -2432, -2432, -2432, + -2432, -2432, -2432, -2080, -2108, -2136, -2164, -2192, -2212, -2232, -2252, -2272, -2270, -2268, -2266, -2264, + -2262, -2260, -2258, -2256, -2269, -2282, -2295, -2308, -2321, -2334, -2347, -2360, -2349, -2338, -2327, -2316, + -2329, -2342, -2355, -2368, -2360, -2352, -2344, -2336, -2448, -2560, -2480, -2400, -2433, -2466, -2435, -2404, + -2405, -2406, -2407, -2408, -2403, -2398, -2393, -2388, -2399, -2410, -2421, -2432, -2432, -2432, -2432, -2432, + -2432, -2432, -2432, -2080, -2108, -2136, -2164, -2192, -2212, -2232, -2252, -2272, -2270, -2268, -2266, -2264, + -2262, -2260, -2258, -2256, -2269, -2282, -2295, -2308, -2321, -2334, -2347, -2360, -2349, -2338, -2327, -2316, + -2329, -2342, -2355, -2368, -2360, -2352, -2344, -2336, -2448, -2560, -2480, -2400, -2433, -2466, -2435, -2404, + -2405, -2406, -2407, -2408, -2403, -2398, -2393, -2388, -2399, -2410, -2421, -2432, -2432, -2432, -2432, -2432, + -2432, -2432, -2432, +]; + +// 4.2.4.4 +const XRGB_BUFFER: [u8; 4 * 64 * 64] = [ + 0x00, 0x22, 0x9c, 0xdf, 0x00, 0x24, 0x9d, 0xe0, 0x00, 0x25, 0x9f, 0xe2, 0x00, 0x2c, 0xa5, 0xe8, 0x00, 0x22, 0x9c, + 0xdf, 0x00, 0x22, 0x9c, 0xe0, 0x00, 0x23, 0x9d, 0xe0, 0x00, 0x22, 0x9c, 0xe0, 0x00, 0x22, 0x9c, 0xdf, 0x00, 0x22, + 0x9c, 0xdf, 0x00, 0x23, 0x9c, 0xe0, 0x00, 0x24, 0x9c, 0xe0, 0x00, 0x24, 0x9c, 0xe0, 0x00, 0x21, 0x9c, 0xe3, 0x00, + 0x1e, 0x9c, 0xe6, 0x00, 0x20, 0x9a, 0xe2, 0x00, 0x22, 0x99, 0xdd, 0x00, 0x21, 0x99, 0xde, 0x00, 0x20, 0x9a, 0xdf, + 0x00, 0x20, 0x9a, 0xe0, 0x00, 0x1f, 0x9b, 0xe0, 0x00, 0x1e, 0x9a, 0xe0, 0x00, 0x1d, 0x99, 0xe0, 0x00, 0x1c, 0x98, + 0xe0, 0x00, 0x1b, 0x97, 0xdf, 0x00, 0x1e, 0x96, 0xdc, 0x00, 0x21, 0x94, 0xd9, 0x00, 0x1f, 0x93, 0xdd, 0x00, 0x1d, + 0x93, 0xe0, 0x00, 0x1b, 0x94, 0xdc, 0x00, 0x18, 0x95, 0xd8, 0x00, 0x1c, 0x92, 0xdb, 0x00, 0x20, 0x8f, 0xde, 0x00, + 0x1b, 0x91, 0xde, 0x00, 0x16, 0x93, 0xdf, 0x00, 0x17, 0x93, 0xdf, 0x00, 0x19, 0x92, 0xdf, 0x00, 0x18, 0x91, 0xdf, + 0x00, 0x17, 0x8f, 0xdf, 0x00, 0x17, 0x8e, 0xdf, 0x00, 0x16, 0x8d, 0xde, 0x00, 0x15, 0x8c, 0xdd, 0x00, 0x14, 0x8c, + 0xdc, 0x00, 0x12, 0x8c, 0xda, 0x00, 0x11, 0x8c, 0xd9, 0x00, 0x11, 0x8b, 0xd9, 0x00, 0x12, 0x8a, 0xda, 0x00, 0x12, + 0x89, 0xda, 0x00, 0x12, 0x88, 0xdb, 0x00, 0x11, 0x87, 0xda, 0x00, 0x11, 0x86, 0xda, 0x00, 0x10, 0x85, 0xda, 0x00, + 0x0f, 0x85, 0xd9, 0x00, 0x0f, 0x84, 0xd9, 0x00, 0x0e, 0x83, 0xd9, 0x00, 0x0d, 0x82, 0xd8, 0x00, 0x0d, 0x82, 0xd8, + 0x00, 0x0d, 0x81, 0xd8, 0x00, 0x0d, 0x80, 0xd7, 0x00, 0x0d, 0x7f, 0xd7, 0x00, 0x0d, 0x7e, 0xd6, 0x00, 0x0d, 0x7e, + 0xd6, 0x00, 0x0d, 0x7e, 0xd6, 0x00, 0x0d, 0x7e, 0xd6, 0x00, 0x25, 0x9f, 0xe1, 0x00, 0x27, 0xa1, 0xe2, 0x00, 0x29, + 0xa2, 0xe3, 0x00, 0x2b, 0xa4, 0xe6, 0x00, 0x24, 0x9f, 0xe1, 0x00, 0x24, 0x9f, 0xe1, 0x00, 0x24, 0x9f, 0xe1, 0x00, + 0x24, 0x9e, 0xe1, 0x00, 0x23, 0x9e, 0xe1, 0x00, 0x24, 0x9e, 0xe1, 0x00, 0x24, 0x9e, 0xe1, 0x00, 0x25, 0x9d, 0xe1, + 0x00, 0x25, 0x9d, 0xe2, 0x00, 0x24, 0x9d, 0xe2, 0x00, 0x22, 0x9d, 0xe2, 0x00, 0x22, 0x9c, 0xe1, 0x00, 0x22, 0x9b, + 0xdf, 0x00, 0x21, 0x9c, 0xe0, 0x00, 0x20, 0x9c, 0xe1, 0x00, 0x20, 0x9c, 0xe2, 0x00, 0x20, 0x9c, 0xe2, 0x00, 0x20, + 0x9a, 0xe0, 0x00, 0x21, 0x99, 0xde, 0x00, 0x1f, 0x99, 0xdf, 0x00, 0x1d, 0x98, 0xe0, 0x00, 0x1e, 0x97, 0xe0, 0x00, + 0x1f, 0x97, 0xe0, 0x00, 0x1d, 0x96, 0xdf, 0x00, 0x1c, 0x95, 0xde, 0x00, 0x1c, 0x94, 0xe0, 0x00, 0x1c, 0x94, 0xe1, + 0x00, 0x1d, 0x93, 0xe1, 0x00, 0x1d, 0x92, 0xe0, 0x00, 0x1b, 0x93, 0xde, 0x00, 0x1a, 0x94, 0xdc, 0x00, 0x1a, 0x93, + 0xde, 0x00, 0x1a, 0x93, 0xe0, 0x00, 0x19, 0x92, 0xe0, 0x00, 0x18, 0x91, 0xdf, 0x00, 0x18, 0x8f, 0xdf, 0x00, 0x17, + 0x8e, 0xdf, 0x00, 0x16, 0x8e, 0xde, 0x00, 0x15, 0x8e, 0xdd, 0x00, 0x14, 0x8d, 0xdc, 0x00, 0x13, 0x8d, 0xdb, 0x00, + 0x13, 0x8c, 0xdb, 0x00, 0x13, 0x8b, 0xdb, 0x00, 0x12, 0x8a, 0xdb, 0x00, 0x12, 0x89, 0xdb, 0x00, 0x12, 0x88, 0xdb, + 0x00, 0x11, 0x87, 0xdb, 0x00, 0x11, 0x86, 0xdb, 0x00, 0x10, 0x85, 0xdb, 0x00, 0x0f, 0x84, 0xda, 0x00, 0x0e, 0x83, + 0xd9, 0x00, 0x0e, 0x83, 0xd9, 0x00, 0x0e, 0x83, 0xd9, 0x00, 0x0e, 0x82, 0xd9, 0x00, 0x0e, 0x81, 0xd8, 0x00, 0x0e, + 0x80, 0xd8, 0x00, 0x0d, 0x7f, 0xd7, 0x00, 0x0d, 0x7f, 0xd7, 0x00, 0x0d, 0x7f, 0xd7, 0x00, 0x0d, 0x7f, 0xd7, 0x00, + 0x27, 0xa3, 0xe3, 0x00, 0x2a, 0xa4, 0xe3, 0x00, 0x2e, 0xa6, 0xe3, 0x00, 0x2a, 0xa4, 0xe3, 0x00, 0x26, 0xa2, 0xe3, + 0x00, 0x26, 0xa1, 0xe3, 0x00, 0x25, 0xa1, 0xe3, 0x00, 0x25, 0xa0, 0xe3, 0x00, 0x25, 0xa0, 0xe3, 0x00, 0x25, 0xa0, + 0xe3, 0x00, 0x25, 0x9f, 0xe3, 0x00, 0x26, 0x9f, 0xe3, 0x00, 0x26, 0x9e, 0xe4, 0x00, 0x27, 0x9e, 0xe1, 0x00, 0x27, + 0x9e, 0xdf, 0x00, 0x25, 0x9e, 0xe0, 0x00, 0x23, 0x9e, 0xe1, 0x00, 0x21, 0x9e, 0xe2, 0x00, 0x20, 0x9e, 0xe4, 0x00, + 0x20, 0x9d, 0xe4, 0x00, 0x21, 0x9d, 0xe3, 0x00, 0x22, 0x9b, 0xe0, 0x00, 0x24, 0x99, 0xdc, 0x00, 0x22, 0x99, 0xde, + 0x00, 0x1f, 0x98, 0xe0, 0x00, 0x1d, 0x99, 0xe4, 0x00, 0x1b, 0x9a, 0xe7, 0x00, 0x1c, 0x98, 0xe2, 0x00, 0x1c, 0x96, + 0xdc, 0x00, 0x1e, 0x94, 0xe3, 0x00, 0x20, 0x92, 0xea, 0x00, 0x1d, 0x94, 0xe6, 0x00, 0x1a, 0x96, 0xe2, 0x00, 0x1c, + 0x96, 0xde, 0x00, 0x1d, 0x95, 0xda, 0x00, 0x1c, 0x94, 0xde, 0x00, 0x1b, 0x94, 0xe1, 0x00, 0x1a, 0x93, 0xe0, 0x00, + 0x1a, 0x92, 0xe0, 0x00, 0x19, 0x91, 0xe0, 0x00, 0x18, 0x90, 0xe0, 0x00, 0x17, 0x90, 0xdf, 0x00, 0x17, 0x8f, 0xde, + 0x00, 0x16, 0x8f, 0xde, 0x00, 0x15, 0x8e, 0xdd, 0x00, 0x14, 0x8d, 0xdd, 0x00, 0x13, 0x8c, 0xdc, 0x00, 0x13, 0x8b, + 0xdc, 0x00, 0x12, 0x8a, 0xdc, 0x00, 0x12, 0x89, 0xdc, 0x00, 0x11, 0x88, 0xdc, 0x00, 0x11, 0x87, 0xdd, 0x00, 0x10, + 0x86, 0xdd, 0x00, 0x0f, 0x85, 0xdb, 0x00, 0x0e, 0x83, 0xd9, 0x00, 0x0e, 0x84, 0xda, 0x00, 0x0f, 0x84, 0xda, 0x00, + 0x0e, 0x83, 0xda, 0x00, 0x0e, 0x82, 0xd9, 0x00, 0x0e, 0x81, 0xd9, 0x00, 0x0e, 0x80, 0xd8, 0x00, 0x0e, 0x80, 0xd8, + 0x00, 0x0e, 0x80, 0xd8, 0x00, 0x0e, 0x80, 0xd8, 0x00, 0x2a, 0xa7, 0xe5, 0x00, 0x2d, 0xa7, 0xe4, 0x00, 0x31, 0xa8, + 0xe3, 0x00, 0x2c, 0xa6, 0xe3, 0x00, 0x27, 0xa4, 0xe4, 0x00, 0x27, 0xa3, 0xe4, 0x00, 0x27, 0xa3, 0xe4, 0x00, 0x27, + 0xa3, 0xe4, 0x00, 0x26, 0xa2, 0xe4, 0x00, 0x26, 0xa2, 0xe4, 0x00, 0x27, 0xa1, 0xe5, 0x00, 0x27, 0xa0, 0xe5, 0x00, + 0x27, 0xa0, 0xe6, 0x00, 0x26, 0xa0, 0xe5, 0x00, 0x25, 0xa0, 0xe4, 0x00, 0x25, 0x9f, 0xe4, 0x00, 0x25, 0x9e, 0xe3, + 0x00, 0x23, 0x9e, 0xe5, 0x00, 0x22, 0x9f, 0xe6, 0x00, 0x22, 0x9f, 0xe5, 0x00, 0x22, 0x9f, 0xe4, 0x00, 0x13, 0xa5, + 0xe6, 0x00, 0x1b, 0x9f, 0xe8, 0x00, 0x16, 0xa0, 0xe8, 0x00, 0x11, 0xa0, 0xe7, 0x00, 0x12, 0x9f, 0xef, 0x00, 0x13, + 0x9e, 0xf7, 0x00, 0x1b, 0x99, 0xec, 0x00, 0x17, 0x9a, 0xe2, 0x00, 0x14, 0x9c, 0xe4, 0x00, 0x1d, 0x98, 0xe5, 0x00, + 0x1c, 0x97, 0xe6, 0x00, 0x1b, 0x96, 0xe7, 0x00, 0x1c, 0x98, 0xdc, 0x00, 0x1d, 0x97, 0xdf, 0x00, 0x1c, 0x96, 0xe1, + 0x00, 0x1c, 0x94, 0xe2, 0x00, 0x1b, 0x94, 0xe1, 0x00, 0x1b, 0x93, 0xe1, 0x00, 0x1a, 0x93, 0xe0, 0x00, 0x1a, 0x92, + 0xe0, 0x00, 0x19, 0x91, 0xe0, 0x00, 0x18, 0x90, 0xe0, 0x00, 0x17, 0x90, 0xdf, 0x00, 0x16, 0x8f, 0xdf, 0x00, 0x15, + 0x8e, 0xde, 0x00, 0x15, 0x8d, 0xde, 0x00, 0x14, 0x8c, 0xdd, 0x00, 0x13, 0x8b, 0xdc, 0x00, 0x12, 0x8a, 0xdd, 0x00, + 0x12, 0x89, 0xdd, 0x00, 0x11, 0x88, 0xde, 0x00, 0x11, 0x87, 0xde, 0x00, 0x0f, 0x85, 0xdc, 0x00, 0x0d, 0x83, 0xda, + 0x00, 0x0f, 0x85, 0xdb, 0x00, 0x10, 0x86, 0xdb, 0x00, 0x0f, 0x84, 0xdb, 0x00, 0x0f, 0x83, 0xda, 0x00, 0x0e, 0x82, + 0xda, 0x00, 0x0e, 0x81, 0xda, 0x00, 0x0e, 0x81, 0xda, 0x00, 0x0e, 0x81, 0xda, 0x00, 0x0e, 0x81, 0xda, 0x00, 0x2c, + 0xaa, 0xe7, 0x00, 0x30, 0xaa, 0xe5, 0x00, 0x34, 0xab, 0xe3, 0x00, 0x2e, 0xa8, 0xe4, 0x00, 0x29, 0xa6, 0xe5, 0x00, + 0x28, 0xa6, 0xe5, 0x00, 0x28, 0xa5, 0xe5, 0x00, 0x28, 0xa5, 0xe5, 0x00, 0x28, 0xa5, 0xe6, 0x00, 0x28, 0xa4, 0xe6, + 0x00, 0x28, 0xa3, 0xe7, 0x00, 0x28, 0xa2, 0xe7, 0x00, 0x28, 0xa1, 0xe8, 0x00, 0x25, 0xa2, 0xe9, 0x00, 0x23, 0xa3, + 0xea, 0x00, 0x25, 0xa0, 0xe8, 0x00, 0x27, 0x9e, 0xe6, 0x00, 0x25, 0x9f, 0xe7, 0x00, 0x23, 0xa0, 0xe9, 0x00, 0x18, + 0xa4, 0xf5, 0x00, 0x0e, 0xa7, 0xff, 0x00, 0x1b, 0xa6, 0xde, 0x00, 0x55, 0x8e, 0xbb, 0x00, 0x6f, 0x83, 0x9c, 0x00, + 0x89, 0x79, 0x7e, 0x00, 0x8d, 0x79, 0x7c, 0x00, 0x91, 0x79, 0x79, 0x00, 0x7f, 0x7b, 0x94, 0x00, 0x56, 0x87, 0xaf, + 0x00, 0x22, 0x9b, 0xd6, 0x00, 0x04, 0xa4, 0xfd, 0x00, 0x10, 0x9d, 0xf4, 0x00, 0x1c, 0x97, 0xeb, 0x00, 0x1c, 0x9a, + 0xda, 0x00, 0x1c, 0x98, 0xe4, 0x00, 0x1c, 0x97, 0xe3, 0x00, 0x1d, 0x95, 0xe2, 0x00, 0x1c, 0x95, 0xe2, 0x00, 0x1c, + 0x94, 0xe2, 0x00, 0x1c, 0x94, 0xe1, 0x00, 0x1b, 0x94, 0xe1, 0x00, 0x1a, 0x93, 0xe1, 0x00, 0x1a, 0x92, 0xe1, 0x00, + 0x19, 0x91, 0xe1, 0x00, 0x18, 0x90, 0xe1, 0x00, 0x17, 0x8f, 0xe0, 0x00, 0x15, 0x8e, 0xdf, 0x00, 0x14, 0x8d, 0xde, + 0x00, 0x13, 0x8c, 0xdd, 0x00, 0x12, 0x8b, 0xde, 0x00, 0x12, 0x8a, 0xdf, 0x00, 0x12, 0x89, 0xdf, 0x00, 0x11, 0x88, + 0xe0, 0x00, 0x0f, 0x85, 0xdd, 0x00, 0x0d, 0x83, 0xda, 0x00, 0x0f, 0x85, 0xdb, 0x00, 0x11, 0x87, 0xdd, 0x00, 0x10, + 0x86, 0xdc, 0x00, 0x0f, 0x84, 0xdc, 0x00, 0x0e, 0x83, 0xdb, 0x00, 0x0e, 0x81, 0xdb, 0x00, 0x0e, 0x81, 0xdb, 0x00, + 0x0e, 0x81, 0xdb, 0x00, 0x0e, 0x81, 0xdb, 0x00, 0x30, 0xab, 0xe5, 0x00, 0x36, 0xaf, 0xe8, 0x00, 0x34, 0xab, 0xe4, + 0x00, 0x2f, 0xaa, 0xe5, 0x00, 0x2b, 0xa8, 0xe6, 0x00, 0x36, 0xae, 0xe8, 0x00, 0x26, 0xa6, 0xe8, 0x00, 0x29, 0xa7, + 0xe7, 0x00, 0x2c, 0xa8, 0xe7, 0x00, 0x2d, 0xa7, 0xe6, 0x00, 0x2f, 0xa5, 0xe5, 0x00, 0x2c, 0xa5, 0xe7, 0x00, 0x29, + 0xa4, 0xe9, 0x00, 0x2b, 0xa5, 0xe5, 0x00, 0x2c, 0xa5, 0xe2, 0x00, 0x10, 0xaa, 0xef, 0x00, 0x13, 0xad, 0xf6, 0x00, + 0x23, 0xa3, 0xf8, 0x00, 0x60, 0x91, 0xa5, 0x00, 0xa6, 0x75, 0x5d, 0x00, 0xec, 0x59, 0x15, 0x00, 0xff, 0x49, 0x0c, + 0x00, 0xfa, 0x55, 0x04, 0x00, 0xff, 0x59, 0x0f, 0x00, 0xff, 0x5d, 0x1b, 0x00, 0xff, 0x61, 0x16, 0x00, 0xfa, 0x64, + 0x12, 0x00, 0xff, 0x55, 0x0f, 0x00, 0xff, 0x4b, 0x0d, 0x00, 0xfb, 0x49, 0x18, 0x00, 0xf5, 0x48, 0x23, 0x00, 0x8e, + 0x73, 0x7e, 0x00, 0x26, 0x9e, 0xda, 0x00, 0x06, 0xa2, 0xff, 0x00, 0x1d, 0x97, 0xe2, 0x00, 0x17, 0x99, 0xea, 0x00, + 0x1c, 0x97, 0xe4, 0x00, 0x1a, 0x98, 0xe4, 0x00, 0x18, 0x98, 0xe4, 0x00, 0x1a, 0x96, 0xe3, 0x00, 0x1b, 0x95, 0xe3, + 0x00, 0x1a, 0x94, 0xe2, 0x00, 0x1a, 0x93, 0xe0, 0x00, 0x19, 0x92, 0xe1, 0x00, 0x18, 0x91, 0xe2, 0x00, 0x17, 0x90, + 0xe1, 0x00, 0x16, 0x8f, 0xe0, 0x00, 0x15, 0x8f, 0xdf, 0x00, 0x13, 0x8e, 0xde, 0x00, 0x13, 0x8d, 0xdf, 0x00, 0x13, + 0x8c, 0xe0, 0x00, 0x12, 0x8b, 0xe0, 0x00, 0x11, 0x89, 0xe0, 0x00, 0x10, 0x87, 0xde, 0x00, 0x0f, 0x85, 0xdb, 0x00, + 0x13, 0x8a, 0xe0, 0x00, 0x0f, 0x87, 0xdc, 0x00, 0x0f, 0x86, 0xdc, 0x00, 0x0f, 0x85, 0xdc, 0x00, 0x0f, 0x84, 0xdc, + 0x00, 0x0e, 0x83, 0xdb, 0x00, 0x0e, 0x83, 0xdb, 0x00, 0x0e, 0x83, 0xdb, 0x00, 0x0e, 0x83, 0xdb, 0x00, 0x34, 0xab, + 0xe2, 0x00, 0x3c, 0xb4, 0xec, 0x00, 0x34, 0xac, 0xe5, 0x00, 0x31, 0xab, 0xe6, 0x00, 0x2d, 0xaa, 0xe8, 0x00, 0x44, + 0xb6, 0xeb, 0x00, 0x24, 0xa7, 0xea, 0x00, 0x29, 0xaa, 0xea, 0x00, 0x2f, 0xac, 0xe9, 0x00, 0x32, 0xa9, 0xe6, 0x00, + 0x35, 0xa7, 0xe3, 0x00, 0x30, 0xa7, 0xe6, 0x00, 0x2b, 0xa8, 0xea, 0x00, 0x25, 0xaa, 0xf0, 0x00, 0x20, 0xad, 0xf6, + 0x00, 0x4d, 0x8b, 0xa7, 0x00, 0xb8, 0x67, 0x4c, 0x00, 0xff, 0x55, 0x10, 0x00, 0xf7, 0x65, 0x0c, 0x00, 0xf8, 0x63, + 0x13, 0x00, 0xfa, 0x61, 0x1b, 0x00, 0xf0, 0x67, 0x1f, 0x00, 0xfc, 0x62, 0x22, 0x00, 0xfb, 0x69, 0x26, 0x00, 0xf9, + 0x6f, 0x29, 0x00, 0xf6, 0x71, 0x22, 0x00, 0xf3, 0x72, 0x1b, 0x00, 0xf2, 0x6b, 0x20, 0x00, 0xf1, 0x64, 0x24, 0x00, + 0xff, 0x56, 0x22, 0x00, 0xff, 0x53, 0x1f, 0x00, 0xff, 0x4b, 0x17, 0x00, 0xff, 0x44, 0x0e, 0x00, 0xb1, 0x61, 0x5b, + 0x00, 0x1f, 0x95, 0xe0, 0x00, 0x12, 0x9b, 0xf0, 0x00, 0x1c, 0x9a, 0xe5, 0x00, 0x18, 0x9a, 0xe6, 0x00, 0x15, 0x9b, + 0xe7, 0x00, 0x18, 0x98, 0xe6, 0x00, 0x1b, 0x95, 0xe5, 0x00, 0x1b, 0x95, 0xe2, 0x00, 0x19, 0x95, 0xe0, 0x00, 0x19, + 0x94, 0xe1, 0x00, 0x18, 0x92, 0xe2, 0x00, 0x17, 0x92, 0xe1, 0x00, 0x16, 0x91, 0xe0, 0x00, 0x15, 0x90, 0xdf, 0x00, + 0x14, 0x8f, 0xdf, 0x00, 0x14, 0x8f, 0xe0, 0x00, 0x14, 0x8f, 0xe1, 0x00, 0x12, 0x8d, 0xe1, 0x00, 0x10, 0x8b, 0xe0, + 0x00, 0x11, 0x89, 0xde, 0x00, 0x11, 0x86, 0xdd, 0x00, 0x17, 0x8f, 0xe4, 0x00, 0x0e, 0x87, 0xdb, 0x00, 0x0e, 0x87, + 0xdc, 0x00, 0x0f, 0x87, 0xdd, 0x00, 0x0f, 0x85, 0xdc, 0x00, 0x0e, 0x84, 0xdc, 0x00, 0x0e, 0x84, 0xdc, 0x00, 0x0e, + 0x84, 0xdc, 0x00, 0x0e, 0x84, 0xdc, 0x00, 0x36, 0xb1, 0xeb, 0x00, 0x36, 0xb4, 0xf0, 0x00, 0x2e, 0xaf, 0xed, 0x00, + 0x2c, 0xae, 0xec, 0x00, 0x2a, 0xad, 0xec, 0x00, 0x41, 0xb4, 0xef, 0x00, 0x29, 0xab, 0xe9, 0x00, 0x2c, 0xab, 0xe8, + 0x00, 0x2f, 0xab, 0xe7, 0x00, 0x31, 0xab, 0xe6, 0x00, 0x32, 0xaa, 0xe6, 0x00, 0x2f, 0xaa, 0xe7, 0x00, 0x2c, 0xa9, + 0xe8, 0x00, 0x25, 0xa7, 0xeb, 0x00, 0x94, 0x6a, 0x5f, 0x00, 0xff, 0x3e, 0x06, 0x00, 0xf9, 0x56, 0x18, 0x00, 0xe2, + 0x73, 0x12, 0x00, 0xf8, 0x73, 0x29, 0x00, 0xf7, 0x74, 0x27, 0x00, 0xf7, 0x76, 0x26, 0x00, 0xf2, 0x76, 0x28, 0x00, + 0xf8, 0x71, 0x2b, 0x00, 0xf9, 0x77, 0x2e, 0x00, 0xf9, 0x7e, 0x30, 0x00, 0xf7, 0x7f, 0x2e, 0x00, 0xf5, 0x81, 0x2b, + 0x00, 0xf5, 0x7b, 0x2c, 0x00, 0xf5, 0x75, 0x2d, 0x00, 0xfd, 0x6a, 0x2b, 0x00, 0xfb, 0x65, 0x2a, 0x00, 0xf6, 0x5e, + 0x2c, 0x00, 0xf1, 0x57, 0x2e, 0x00, 0xff, 0x48, 0x10, 0x00, 0xff, 0x46, 0x0f, 0x00, 0x81, 0x76, 0x80, 0x00, 0x02, + 0xa7, 0xf1, 0x00, 0x24, 0x96, 0xea, 0x00, 0x19, 0x9b, 0xe4, 0x00, 0x1b, 0x98, 0xe4, 0x00, 0x1d, 0x96, 0xe5, 0x00, + 0x1b, 0x96, 0xe2, 0x00, 0x1a, 0x96, 0xe0, 0x00, 0x19, 0x95, 0xe1, 0x00, 0x17, 0x94, 0xe3, 0x00, 0x17, 0x93, 0xe2, + 0x00, 0x16, 0x92, 0xe1, 0x00, 0x16, 0x91, 0xe0, 0x00, 0x15, 0x90, 0xdf, 0x00, 0x15, 0x91, 0xe1, 0x00, 0x15, 0x91, + 0xe3, 0x00, 0x13, 0x8f, 0xe1, 0x00, 0x10, 0x8c, 0xe0, 0x00, 0x12, 0x8b, 0xe0, 0x00, 0x15, 0x8a, 0xe0, 0x00, 0x16, + 0x8d, 0xe2, 0x00, 0x0f, 0x89, 0xdd, 0x00, 0x0f, 0x88, 0xdd, 0x00, 0x0f, 0x88, 0xdd, 0x00, 0x0f, 0x86, 0xdd, 0x00, + 0x0f, 0x85, 0xdc, 0x00, 0x0f, 0x85, 0xdc, 0x00, 0x0f, 0x85, 0xdc, 0x00, 0x0f, 0x85, 0xdc, 0x00, 0x5f, 0xc1, 0xe7, + 0x00, 0x57, 0xbe, 0xe8, 0x00, 0x4f, 0xbb, 0xe9, 0x00, 0x4e, 0xba, 0xe6, 0x00, 0x4e, 0xba, 0xe3, 0x00, 0x51, 0xb6, + 0xee, 0x00, 0x2e, 0xae, 0xe8, 0x00, 0x2e, 0xad, 0xe6, 0x00, 0x2f, 0xab, 0xe5, 0x00, 0x2f, 0xac, 0xe7, 0x00, 0x2e, + 0xad, 0xe9, 0x00, 0x2e, 0xac, 0xe7, 0x00, 0x2d, 0xaa, 0xe5, 0x00, 0x15, 0xb2, 0xff, 0x00, 0xec, 0x43, 0x10, 0x00, + 0xf1, 0x50, 0x16, 0x00, 0xf7, 0x5d, 0x1c, 0x00, 0xf8, 0x71, 0x23, 0x00, 0xf9, 0x86, 0x2a, 0x00, 0xf6, 0x88, 0x2d, + 0x00, 0xf4, 0x8b, 0x31, 0x00, 0xf4, 0x85, 0x32, 0x00, 0xf4, 0x7f, 0x33, 0x00, 0xf7, 0x85, 0x35, 0x00, 0xfa, 0x8c, + 0x37, 0x00, 0xf8, 0x8e, 0x39, 0x00, 0xf7, 0x90, 0x3a, 0x00, 0xf8, 0x8b, 0x38, 0x00, 0xf9, 0x86, 0x35, 0x00, 0xf8, + 0x7e, 0x35, 0x00, 0xf7, 0x76, 0x35, 0x00, 0xf7, 0x6d, 0x34, 0x00, 0xf7, 0x65, 0x32, 0x00, 0xf8, 0x5e, 0x31, 0x00, + 0xf9, 0x57, 0x30, 0x00, 0xff, 0x51, 0x25, 0x00, 0xf6, 0x52, 0x37, 0x00, 0x03, 0xa5, 0xfd, 0x00, 0x1e, 0x9b, 0xe1, + 0x00, 0x1e, 0x98, 0xe3, 0x00, 0x1f, 0x96, 0xe5, 0x00, 0x1c, 0x97, 0xe2, 0x00, 0x1a, 0x97, 0xdf, 0x00, 0x18, 0x96, + 0xe1, 0x00, 0x17, 0x95, 0xe4, 0x00, 0x17, 0x94, 0xe3, 0x00, 0x17, 0x93, 0xe2, 0x00, 0x16, 0x92, 0xe1, 0x00, 0x16, + 0x92, 0xe0, 0x00, 0x16, 0x93, 0xe2, 0x00, 0x17, 0x94, 0xe4, 0x00, 0x13, 0x91, 0xe2, 0x00, 0x0f, 0x8e, 0xe0, 0x00, + 0x14, 0x8e, 0xe1, 0x00, 0x19, 0x8e, 0xe3, 0x00, 0x14, 0x8c, 0xe1, 0x00, 0x0f, 0x8b, 0xde, 0x00, 0x0f, 0x8a, 0xde, + 0x00, 0x0f, 0x89, 0xde, 0x00, 0x0f, 0x88, 0xdd, 0x00, 0x0f, 0x86, 0xdd, 0x00, 0x0f, 0x86, 0xdd, 0x00, 0x0f, 0x86, + 0xdd, 0x00, 0x0f, 0x86, 0xdd, 0x00, 0x3c, 0xb6, 0xee, 0x00, 0x36, 0xb4, 0xef, 0x00, 0x30, 0xb2, 0xf0, 0x00, 0x30, + 0xb1, 0xee, 0x00, 0x2f, 0xb1, 0xec, 0x00, 0x38, 0xb0, 0xef, 0x00, 0x2e, 0xae, 0xe9, 0x00, 0x2f, 0xae, 0xe8, 0x00, + 0x31, 0xad, 0xe6, 0x00, 0x2f, 0xaf, 0xe8, 0x00, 0x2e, 0xb1, 0xea, 0x00, 0x31, 0xad, 0xec, 0x00, 0x29, 0xaf, 0xee, + 0x00, 0x30, 0xaa, 0xc8, 0x00, 0xff, 0x3d, 0x05, 0x00, 0xfa, 0x50, 0x1a, 0x00, 0xf9, 0x60, 0x21, 0x00, 0xf8, 0x74, + 0x28, 0x00, 0xf7, 0x88, 0x2f, 0x00, 0xfa, 0x96, 0x38, 0x00, 0xf5, 0x9b, 0x38, 0x00, 0xf5, 0x97, 0x3b, 0x00, 0xf6, + 0x92, 0x3e, 0x00, 0xf8, 0x94, 0x40, 0x00, 0xfa, 0x97, 0x42, 0x00, 0xfa, 0x9a, 0x44, 0x00, 0xfa, 0x9d, 0x46, 0x00, + 0xf9, 0x98, 0x45, 0x00, 0xf8, 0x94, 0x44, 0x00, 0xf9, 0x8d, 0x43, 0x00, 0xfa, 0x86, 0x41, 0x00, 0xf9, 0x7d, 0x3f, + 0x00, 0xf9, 0x74, 0x3d, 0x00, 0xf7, 0x70, 0x39, 0x00, 0xf5, 0x6d, 0x35, 0x00, 0xff, 0x61, 0x22, 0x00, 0xbf, 0x6c, + 0x63, 0x00, 0x12, 0x9e, 0xef, 0x00, 0x22, 0x9a, 0xe8, 0x00, 0x1c, 0x99, 0xed, 0x00, 0x17, 0x9c, 0xe4, 0x00, 0x14, + 0x98, 0xf0, 0x00, 0x1b, 0x94, 0xe1, 0x00, 0x1a, 0x96, 0xe2, 0x00, 0x19, 0x98, 0xe3, 0x00, 0x18, 0x97, 0xe4, 0x00, + 0x18, 0x96, 0xe5, 0x00, 0x18, 0x95, 0xe4, 0x00, 0x19, 0x93, 0xe2, 0x00, 0x17, 0x92, 0xe1, 0x00, 0x15, 0x90, 0xdf, + 0x00, 0x16, 0x92, 0xe2, 0x00, 0x17, 0x93, 0xe5, 0x00, 0x14, 0x90, 0xe4, 0x00, 0x12, 0x8e, 0xe2, 0x00, 0x11, 0x8d, + 0xe3, 0x00, 0x10, 0x8d, 0xe3, 0x00, 0x11, 0x8b, 0xde, 0x00, 0x12, 0x89, 0xd9, 0x00, 0x0f, 0x88, 0xe2, 0x00, 0x0c, + 0x89, 0xdd, 0x00, 0x10, 0x85, 0xe0, 0x00, 0x09, 0x87, 0xe4, 0x00, 0x09, 0x87, 0xe4, 0x00, 0x40, 0xb5, 0xe9, 0x00, + 0x3b, 0xb4, 0xe9, 0x00, 0x37, 0xb2, 0xea, 0x00, 0x37, 0xb2, 0xe9, 0x00, 0x38, 0xb1, 0xe8, 0x00, 0x33, 0xb0, 0xea, + 0x00, 0x2e, 0xae, 0xeb, 0x00, 0x30, 0xaf, 0xe9, 0x00, 0x33, 0xaf, 0xe8, 0x00, 0x30, 0xb2, 0xea, 0x00, 0x2e, 0xb5, + 0xec, 0x00, 0x34, 0xaf, 0xf2, 0x00, 0x25, 0xb4, 0xf7, 0x00, 0x8d, 0x7f, 0x86, 0x00, 0xf6, 0x4f, 0x00, 0x00, 0xed, + 0x5c, 0x1e, 0x00, 0xfa, 0x63, 0x26, 0x00, 0xf7, 0x76, 0x2d, 0x00, 0xf5, 0x8a, 0x35, 0x00, 0xfe, 0xa2, 0x42, 0x00, + 0xf7, 0xab, 0x3f, 0x00, 0xf7, 0xa8, 0x43, 0x00, 0xf7, 0xa5, 0x48, 0x00, 0xf9, 0xa3, 0x4a, 0x00, 0xfa, 0xa2, 0x4c, + 0x00, 0xfb, 0xa6, 0x4f, 0x00, 0xfc, 0xaa, 0x52, 0x00, 0xf9, 0xa6, 0x52, 0x00, 0xf7, 0xa2, 0x52, 0x00, 0xfa, 0x9c, + 0x50, 0x00, 0xfd, 0x97, 0x4e, 0x00, 0xfc, 0x8d, 0x4b, 0x00, 0xfb, 0x83, 0x48, 0x00, 0xf6, 0x83, 0x41, 0x00, 0xf1, + 0x82, 0x3a, 0x00, 0xf5, 0x73, 0x2c, 0x00, 0x71, 0x8c, 0xac, 0x00, 0x17, 0x9a, 0xf0, 0x00, 0x25, 0x99, 0xef, 0x00, + 0x26, 0x97, 0xe9, 0x00, 0x26, 0x9b, 0xc6, 0x00, 0x16, 0x96, 0xf1, 0x00, 0x1d, 0x91, 0xe3, 0x00, 0x1c, 0x96, 0xe3, + 0x00, 0x1b, 0x9b, 0xe3, 0x00, 0x1a, 0x99, 0xe6, 0x00, 0x19, 0x98, 0xe9, 0x00, 0x1b, 0x97, 0xe7, 0x00, 0x1c, 0x95, + 0xe5, 0x00, 0x18, 0x91, 0xdf, 0x00, 0x13, 0x8d, 0xda, 0x00, 0x19, 0x92, 0xe2, 0x00, 0x1e, 0x98, 0xea, 0x00, 0x15, + 0x92, 0xe6, 0x00, 0x0b, 0x8d, 0xe2, 0x00, 0x0e, 0x8e, 0xe5, 0x00, 0x10, 0x8f, 0xe9, 0x00, 0x12, 0x8c, 0xdf, 0x00, + 0x14, 0x89, 0xd4, 0x00, 0x0e, 0x88, 0xe6, 0x00, 0x08, 0x8c, 0xdc, 0x00, 0x11, 0x84, 0xe4, 0x00, 0x04, 0x88, 0xec, + 0x00, 0x04, 0x88, 0xec, 0x00, 0x3e, 0xb6, 0xea, 0x00, 0x3b, 0xb5, 0xeb, 0x00, 0x38, 0xb4, 0xeb, 0x00, 0x38, 0xb4, + 0xeb, 0x00, 0x38, 0xb3, 0xeb, 0x00, 0x35, 0xb2, 0xeb, 0x00, 0x33, 0xb1, 0xec, 0x00, 0x34, 0xb1, 0xeb, 0x00, 0x35, + 0xb1, 0xea, 0x00, 0x32, 0xb3, 0xe9, 0x00, 0x30, 0xb5, 0xe9, 0x00, 0x34, 0xb0, 0xf0, 0x00, 0x23, 0xb6, 0xf8, 0x00, + 0xc5, 0x60, 0x44, 0x00, 0xf9, 0x54, 0x0c, 0x00, 0xf2, 0x63, 0x22, 0x00, 0xf7, 0x70, 0x29, 0x00, 0xf7, 0x7d, 0x2f, + 0x00, 0xf7, 0x8b, 0x35, 0x00, 0xfb, 0xa1, 0x42, 0x00, 0xf6, 0xb0, 0x46, 0x00, 0xfb, 0xb4, 0x4f, 0x00, 0xf7, 0xb0, + 0x51, 0x00, 0xf9, 0xaf, 0x54, 0x00, 0xfb, 0xad, 0x56, 0x00, 0xfc, 0xb2, 0x5a, 0x00, 0xfe, 0xb7, 0x5d, 0x00, 0xfa, + 0xb3, 0x5f, 0x00, 0xf6, 0xb0, 0x61, 0x00, 0xfa, 0xac, 0x5d, 0x00, 0xfd, 0xa9, 0x5a, 0x00, 0xfb, 0x9f, 0x55, 0x00, + 0xf9, 0x95, 0x51, 0x00, 0xf7, 0x91, 0x4b, 0x00, 0xf6, 0x8d, 0x45, 0x00, 0xff, 0x7e, 0x23, 0x00, 0x1b, 0xa5, 0xf0, + 0x00, 0x12, 0x9e, 0xf4, 0x00, 0x28, 0x96, 0xf1, 0x00, 0x23, 0x9f, 0xb1, 0x00, 0x6c, 0x96, 0x00, 0x00, 0x3c, 0x9c, + 0x82, 0x00, 0x17, 0x9e, 0xf8, 0x00, 0x16, 0x9c, 0xf4, 0x00, 0x14, 0x9d, 0xe3, 0x00, 0x16, 0x9a, 0xe5, 0x00, 0x18, + 0x97, 0xe7, 0x00, 0x19, 0x95, 0xe6, 0x00, 0x1a, 0x93, 0xe5, 0x00, 0x19, 0x93, 0xe3, 0x00, 0x17, 0x93, 0xe0, 0x00, + 0x1c, 0x98, 0xe6, 0x00, 0x1a, 0x95, 0xe5, 0x00, 0x16, 0x92, 0xe5, 0x00, 0x13, 0x8f, 0xe5, 0x00, 0x13, 0x8c, 0xeb, + 0x00, 0x13, 0x8b, 0xe3, 0x00, 0x00, 0x87, 0xe4, 0x00, 0x00, 0x7c, 0xf5, 0x00, 0x1a, 0x86, 0xd3, 0x00, 0x0d, 0x8c, + 0xf1, 0x00, 0x00, 0x8f, 0xe2, 0x00, 0x0d, 0x85, 0xea, 0x00, 0x08, 0x86, 0xf1, 0x00, 0x3c, 0xb7, 0xec, 0x00, 0x3b, + 0xb7, 0xed, 0x00, 0x3a, 0xb6, 0xed, 0x00, 0x39, 0xb6, 0xed, 0x00, 0x38, 0xb5, 0xed, 0x00, 0x37, 0xb5, 0xed, 0x00, + 0x37, 0xb4, 0xed, 0x00, 0x37, 0xb3, 0xed, 0x00, 0x36, 0xb3, 0xec, 0x00, 0x34, 0xb4, 0xe9, 0x00, 0x31, 0xb5, 0xe5, + 0x00, 0x35, 0xb1, 0xef, 0x00, 0x21, 0xb8, 0xfa, 0x00, 0xfd, 0x42, 0x03, 0x00, 0xfc, 0x58, 0x1e, 0x00, 0xf8, 0x6a, + 0x26, 0x00, 0xf4, 0x7c, 0x2d, 0x00, 0xf7, 0x84, 0x31, 0x00, 0xf9, 0x8c, 0x36, 0x00, 0xf8, 0xa0, 0x41, 0x00, 0xf6, + 0xb5, 0x4d, 0x00, 0xfe, 0xc0, 0x5b, 0x00, 0xf6, 0xbc, 0x5a, 0x00, 0xf8, 0xba, 0x5d, 0x00, 0xfb, 0xb8, 0x61, 0x00, + 0xfd, 0xbe, 0x65, 0x00, 0xff, 0xc4, 0x69, 0x00, 0xfb, 0xc1, 0x6c, 0x00, 0xf5, 0xbd, 0x70, 0x00, 0xfa, 0xbc, 0x6b, + 0x00, 0xfe, 0xbb, 0x66, 0x00, 0xfa, 0xb1, 0x60, 0x00, 0xf6, 0xa7, 0x5a, 0x00, 0xf8, 0x9f, 0x55, 0x00, 0xfa, 0x98, + 0x4f, 0x00, 0xdf, 0x95, 0x6f, 0x00, 0x08, 0xa6, 0xfc, 0x00, 0x25, 0x9d, 0xdb, 0x00, 0x15, 0x9f, 0xf3, 0x00, 0x4a, + 0xa1, 0x72, 0x00, 0x69, 0xa9, 0x0d, 0x00, 0x62, 0xa4, 0x06, 0x00, 0x5a, 0x98, 0x1b, 0x00, 0x34, 0x96, 0x9b, 0x00, + 0x0e, 0x99, 0xff, 0x00, 0x12, 0x97, 0xf2, 0x00, 0x16, 0x95, 0xe4, 0x00, 0x17, 0x93, 0xe5, 0x00, 0x18, 0x92, 0xe5, + 0x00, 0x19, 0x95, 0xe6, 0x00, 0x1a, 0x98, 0xe7, 0x00, 0x20, 0x9d, 0xeb, 0x00, 0x15, 0x93, 0xdf, 0x00, 0x18, 0x92, + 0xe4, 0x00, 0x1a, 0x91, 0xe9, 0x00, 0x20, 0x95, 0xeb, 0x00, 0x25, 0x9d, 0xd1, 0x00, 0xd0, 0xf7, 0x72, 0x00, 0xc1, + 0xf3, 0x96, 0x00, 0x00, 0x83, 0xf1, 0x00, 0x17, 0x82, 0xa0, 0x00, 0x3c, 0x7e, 0x2f, 0x00, 0x17, 0x87, 0xcc, 0x00, + 0x0b, 0x8a, 0xda, 0x00, 0x3d, 0xb9, 0xed, 0x00, 0x3c, 0xb8, 0xed, 0x00, 0x3b, 0xb8, 0xed, 0x00, 0x3a, 0xb7, 0xed, + 0x00, 0x39, 0xb7, 0xed, 0x00, 0x39, 0xb7, 0xed, 0x00, 0x39, 0xb6, 0xed, 0x00, 0x3a, 0xb6, 0xed, 0x00, 0x3a, 0xb6, + 0xed, 0x00, 0x37, 0xb4, 0xed, 0x00, 0x34, 0xb2, 0xec, 0x00, 0x35, 0xab, 0xf3, 0x00, 0x6e, 0x96, 0xb3, 0x00, 0xff, + 0x46, 0x01, 0x00, 0xf8, 0x65, 0x20, 0x00, 0xf6, 0x73, 0x29, 0x00, 0xf5, 0x81, 0x31, 0x00, 0xf7, 0x8b, 0x37, 0x00, + 0xf9, 0x95, 0x3e, 0x00, 0xf8, 0xa6, 0x49, 0x00, 0xf8, 0xb8, 0x54, 0x00, 0xfc, 0xc2, 0x60, 0x00, 0xf8, 0xc4, 0x65, + 0x00, 0xf9, 0xc3, 0x6a, 0x00, 0xfa, 0xc2, 0x6e, 0x00, 0xfa, 0xc7, 0x73, 0x00, 0xfa, 0xcb, 0x77, 0x00, 0xfb, 0xcb, + 0x7b, 0x00, 0xfc, 0xcb, 0x7e, 0x00, 0xfa, 0xc8, 0x7b, 0x00, 0xf8, 0xc5, 0x78, 0x00, 0xf9, 0xbc, 0x72, 0x00, 0xfb, + 0xb4, 0x6d, 0x00, 0xf6, 0xb0, 0x69, 0x00, 0xfe, 0xaa, 0x57, 0x00, 0x94, 0xa0, 0xa5, 0x00, 0x13, 0xa1, 0xf3, 0x00, + 0x21, 0x9d, 0xf0, 0x00, 0x19, 0x9e, 0xff, 0x00, 0x71, 0xc1, 0x24, 0x00, 0x79, 0xb8, 0x26, 0x00, 0x72, 0xb2, 0x1e, + 0x00, 0x6a, 0xaa, 0x24, 0x00, 0x67, 0xa1, 0x25, 0x00, 0x64, 0x9a, 0x19, 0x00, 0x41, 0x9d, 0x72, 0x00, 0x1f, 0x9f, + 0xcb, 0x00, 0x19, 0x94, 0xff, 0x00, 0x13, 0x99, 0xf1, 0x00, 0x19, 0x9c, 0xf4, 0x00, 0x1e, 0xa0, 0xf8, 0x00, 0x1b, + 0x9c, 0xff, 0x00, 0x11, 0x93, 0xf6, 0x00, 0x12, 0x93, 0xf1, 0x00, 0x13, 0x93, 0xec, 0x00, 0x00, 0x83, 0xff, 0x00, + 0x72, 0xcc, 0xa0, 0x00, 0xcb, 0xf9, 0x82, 0x00, 0xd0, 0xff, 0xac, 0x00, 0x79, 0xa0, 0x46, 0x00, 0x33, 0x77, 0x00, + 0x00, 0x3a, 0x7c, 0x03, 0x00, 0x0d, 0x8d, 0xe2, 0x00, 0x0d, 0x8e, 0xdb, 0x00, 0x3f, 0xbb, 0xee, 0x00, 0x3e, 0xba, + 0xed, 0x00, 0x3d, 0xb9, 0xed, 0x00, 0x3c, 0xb9, 0xed, 0x00, 0x3b, 0xb8, 0xed, 0x00, 0x3b, 0xb8, 0xed, 0x00, 0x3c, + 0xb9, 0xee, 0x00, 0x3c, 0xb9, 0xee, 0x00, 0x3d, 0xb9, 0xef, 0x00, 0x3a, 0xb4, 0xf1, 0x00, 0x37, 0xaf, 0xf3, 0x00, + 0x32, 0xb3, 0xfe, 0x00, 0xb4, 0x8f, 0x7d, 0x00, 0xff, 0x59, 0x07, 0x00, 0xf3, 0x71, 0x22, 0x00, 0xf5, 0x7c, 0x2b, + 0x00, 0xf6, 0x87, 0x35, 0x00, 0xf7, 0x92, 0x3d, 0x00, 0xf8, 0x9d, 0x45, 0x00, 0xf9, 0xac, 0x50, 0x00, 0xf9, 0xbb, + 0x5a, 0x00, 0xf9, 0xc4, 0x65, 0x00, 0xfa, 0xcd, 0x71, 0x00, 0xfa, 0xcd, 0x76, 0x00, 0xfa, 0xcd, 0x7b, 0x00, 0xf7, + 0xcf, 0x80, 0x00, 0xf4, 0xd2, 0x86, 0x00, 0xfc, 0xd6, 0x89, 0x00, 0xff, 0xd9, 0x8c, 0x00, 0xfb, 0xd4, 0x8b, 0x00, + 0xf3, 0xcf, 0x8a, 0x00, 0xf9, 0xc8, 0x85, 0x00, 0xff, 0xc1, 0x7f, 0x00, 0xf5, 0xc2, 0x7d, 0x00, 0xff, 0xbc, 0x5e, + 0x00, 0x48, 0xab, 0xdc, 0x00, 0x1e, 0x9d, 0xeb, 0x00, 0x1e, 0xa2, 0xe8, 0x00, 0x1d, 0xa8, 0xe5, 0x00, 0x99, 0xd3, + 0x1c, 0x00, 0x8a, 0xcb, 0x22, 0x00, 0x82, 0xc4, 0x27, 0x00, 0x7a, 0xbc, 0x2c, 0x00, 0x75, 0xb4, 0x29, 0x00, 0x70, + 0xad, 0x25, 0x00, 0x6d, 0xab, 0x17, 0x00, 0x6b, 0xa9, 0x08, 0x00, 0x5e, 0xa9, 0x12, 0x00, 0x51, 0x9f, 0x54, 0x00, + 0x48, 0x9b, 0x6d, 0x00, 0x3e, 0x98, 0x87, 0x00, 0x3b, 0x95, 0x92, 0x00, 0x38, 0x98, 0x80, 0x00, 0x44, 0x96, 0x63, + 0x00, 0x50, 0x94, 0x46, 0x00, 0x83, 0xb4, 0x3c, 0x00, 0x4f, 0x85, 0x1b, 0x00, 0xaf, 0xe1, 0x87, 0x00, 0x9f, 0xcc, + 0x83, 0x00, 0x36, 0x80, 0x11, 0x00, 0x43, 0x82, 0x1c, 0x00, 0x32, 0x85, 0x3c, 0x00, 0x04, 0x92, 0xf9, 0x00, 0x10, + 0x92, 0xdd, 0x00, 0x40, 0xbc, 0xee, 0x00, 0x3f, 0xbc, 0xee, 0x00, 0x3e, 0xbb, 0xee, 0x00, 0x3d, 0xba, 0xed, 0x00, + 0x3c, 0xba, 0xed, 0x00, 0x3c, 0xb9, 0xed, 0x00, 0x3c, 0xb9, 0xec, 0x00, 0x3c, 0xb9, 0xec, 0x00, 0x3c, 0xb8, 0xec, + 0x00, 0x3f, 0xb4, 0xf0, 0x00, 0x43, 0xaf, 0xf5, 0x00, 0x0e, 0xbb, 0xe9, 0x00, 0xff, 0xb8, 0x97, 0x00, 0xf7, 0x81, + 0x4d, 0x00, 0xf5, 0x76, 0x23, 0x00, 0xf6, 0x81, 0x2e, 0x00, 0xf8, 0x8c, 0x39, 0x00, 0xf8, 0x99, 0x43, 0x00, 0xf8, + 0xa6, 0x4d, 0x00, 0xf8, 0xb2, 0x57, 0x00, 0xf9, 0xbd, 0x60, 0x00, 0xfa, 0xc9, 0x6d, 0x00, 0xfb, 0xd4, 0x7b, 0x00, + 0xfa, 0xd6, 0x81, 0x00, 0xfa, 0xd7, 0x88, 0x00, 0xfb, 0xd9, 0x8e, 0x00, 0xfb, 0xda, 0x93, 0x00, 0xfa, 0xe5, 0xa1, + 0x00, 0xfe, 0xd6, 0x92, 0x00, 0xfa, 0xde, 0xa0, 0x00, 0xf9, 0xdb, 0x98, 0x00, 0xfa, 0xd6, 0x94, 0x00, 0xfb, 0xd0, + 0x90, 0x00, 0xff, 0xd2, 0x85, 0x00, 0xff, 0xc7, 0x78, 0x00, 0x00, 0x9a, 0xfd, 0x00, 0x26, 0xa8, 0xf2, 0x00, 0x20, + 0xa4, 0xf8, 0x00, 0x53, 0xbe, 0xa5, 0x00, 0xa4, 0xda, 0x31, 0x00, 0x9d, 0xd6, 0x38, 0x00, 0x97, 0xd0, 0x3a, 0x00, + 0x91, 0xca, 0x3d, 0x00, 0x8b, 0xc5, 0x39, 0x00, 0x85, 0xc0, 0x35, 0x00, 0x7d, 0xbe, 0x31, 0x00, 0x74, 0xbc, 0x2d, + 0x00, 0x76, 0xb8, 0x1c, 0x00, 0x77, 0xb0, 0x27, 0x00, 0x72, 0xab, 0x25, 0x00, 0x6d, 0xa7, 0x24, 0x00, 0x6b, 0xa3, + 0x28, 0x00, 0x68, 0xa3, 0x1f, 0x00, 0x58, 0x95, 0x1a, 0x00, 0x78, 0xb7, 0x45, 0x00, 0xbb, 0xf1, 0x81, 0x00, 0x73, + 0xad, 0x4c, 0x00, 0x41, 0x7c, 0x15, 0x00, 0x50, 0x8b, 0x1e, 0x00, 0x43, 0x86, 0x1c, 0x00, 0x49, 0x86, 0x14, 0x00, + 0x17, 0x86, 0x8b, 0x00, 0x0b, 0x90, 0xf6, 0x00, 0x16, 0x8e, 0xe8, 0x00, 0x42, 0xbe, 0xef, 0x00, 0x41, 0xbd, 0xee, + 0x00, 0x40, 0xbc, 0xee, 0x00, 0x3f, 0xbc, 0xed, 0x00, 0x3e, 0xbb, 0xed, 0x00, 0x3d, 0xba, 0xec, 0x00, 0x3d, 0xb9, + 0xeb, 0x00, 0x3c, 0xb8, 0xea, 0x00, 0x3b, 0xb7, 0xe9, 0x00, 0x39, 0xb9, 0xf0, 0x00, 0x37, 0xbb, 0xf7, 0x00, 0x50, + 0xb5, 0xdc, 0x00, 0xff, 0x97, 0x44, 0x00, 0xfe, 0xc4, 0x9d, 0x00, 0xf8, 0x7a, 0x24, 0x00, 0xf8, 0x85, 0x30, 0x00, + 0xf9, 0x91, 0x3d, 0x00, 0xf8, 0xa0, 0x49, 0x00, 0xf7, 0xaf, 0x55, 0x00, 0xf8, 0xb8, 0x5d, 0x00, 0xf9, 0xc0, 0x65, + 0x00, 0xfa, 0xce, 0x75, 0x00, 0xfc, 0xdb, 0x85, 0x00, 0xfb, 0xde, 0x8d, 0x00, 0xfa, 0xe1, 0x95, 0x00, 0xfe, 0xe2, + 0x9b, 0x00, 0xff, 0xe2, 0xa0, 0x00, 0xfb, 0xe9, 0xa4, 0x00, 0xff, 0xbe, 0x6b, 0x00, 0xfd, 0xde, 0x9f, 0x00, 0xff, + 0xe8, 0xa6, 0x00, 0xfb, 0xe3, 0xa3, 0x00, 0xf8, 0xde, 0xa0, 0x00, 0xfd, 0xd8, 0x99, 0x00, 0xb6, 0xbd, 0xab, 0x00, + 0x11, 0x9f, 0xf1, 0x00, 0x1e, 0xa4, 0xe9, 0x00, 0x1a, 0x9f, 0xff, 0x00, 0x89, 0xd4, 0x65, 0x00, 0xb0, 0xe2, 0x45, + 0x00, 0xb0, 0xe0, 0x4e, 0x00, 0xac, 0xdc, 0x4e, 0x00, 0xa7, 0xd9, 0x4e, 0x00, 0xa1, 0xd6, 0x49, 0x00, 0x9a, 0xd3, + 0x45, 0x00, 0x97, 0xce, 0x3d, 0x00, 0x94, 0xc9, 0x35, 0x00, 0x8d, 0xc5, 0x34, 0x00, 0x86, 0xc1, 0x33, 0x00, 0x7b, + 0xbc, 0x32, 0x00, 0x6f, 0xb7, 0x31, 0x00, 0x6d, 0xb3, 0x30, 0x00, 0x6c, 0xae, 0x2e, 0x00, 0x7e, 0xba, 0x3f, 0x00, + 0x70, 0xa5, 0x31, 0x00, 0x7b, 0xb5, 0x4f, 0x00, 0x57, 0x9a, 0x20, 0x00, 0x5c, 0x9f, 0x2b, 0x00, 0x51, 0x94, 0x25, + 0x00, 0x80, 0xb9, 0x65, 0x00, 0x60, 0x9a, 0x1d, 0x00, 0x03, 0x90, 0xe3, 0x00, 0x11, 0x8e, 0xf2, 0x00, 0x1c, 0x89, + 0xf2, 0x00, 0x44, 0xc0, 0xef, 0x00, 0x43, 0xbf, 0xef, 0x00, 0x42, 0xbe, 0xee, 0x00, 0x40, 0xbd, 0xee, 0x00, 0x3f, + 0xbc, 0xee, 0x00, 0x3f, 0xbb, 0xed, 0x00, 0x40, 0xba, 0xeb, 0x00, 0x3e, 0xb9, 0xed, 0x00, 0x3c, 0xb9, 0xee, 0x00, + 0x37, 0xb9, 0xeb, 0x00, 0x27, 0xbc, 0xf7, 0x00, 0x94, 0x9c, 0x8f, 0x00, 0xfb, 0x96, 0x37, 0x00, 0xf9, 0xbc, 0x7c, + 0x00, 0xf9, 0xb5, 0x85, 0x00, 0xf7, 0x99, 0x4a, 0x00, 0xf6, 0x9b, 0x43, 0x00, 0xf6, 0xa6, 0x4e, 0x00, 0xf7, 0xb2, + 0x59, 0x00, 0xf8, 0xbc, 0x66, 0x00, 0xfa, 0xc6, 0x72, 0x00, 0xfa, 0xd3, 0x80, 0x00, 0xfa, 0xe0, 0x8d, 0x00, 0xf9, + 0xe6, 0x98, 0x00, 0xf9, 0xeb, 0xa2, 0x00, 0xfe, 0xea, 0xa6, 0x00, 0xff, 0xea, 0xab, 0x00, 0xfc, 0xef, 0xa9, 0x00, + 0xfa, 0xba, 0x62, 0x00, 0xfb, 0xdc, 0x99, 0x00, 0xff, 0xf4, 0xb9, 0x00, 0xfb, 0xec, 0xb2, 0x00, 0xf7, 0xe6, 0xab, + 0x00, 0xff, 0xe5, 0xa3, 0x00, 0x64, 0xb1, 0xd1, 0x00, 0x19, 0x9f, 0xf0, 0x00, 0x26, 0x9f, 0xe9, 0x00, 0x04, 0x99, + 0xf2, 0x00, 0xe3, 0xf0, 0x51, 0x00, 0xd5, 0xef, 0x58, 0x00, 0xc0, 0xe3, 0x64, 0x00, 0xbd, 0xe1, 0x65, 0x00, 0xba, + 0xe0, 0x65, 0x00, 0xb5, 0xde, 0x5d, 0x00, 0xb0, 0xdc, 0x56, 0x00, 0xaa, 0xd7, 0x4e, 0x00, 0xa3, 0xd3, 0x46, 0x00, + 0x9b, 0xd0, 0x43, 0x00, 0x93, 0xcd, 0x3f, 0x00, 0x8c, 0xc9, 0x3e, 0x00, 0x84, 0xc6, 0x3c, 0x00, 0x81, 0xc1, 0x39, + 0x00, 0x7d, 0xbc, 0x36, 0x00, 0x8b, 0xc7, 0x46, 0x00, 0x89, 0xc2, 0x45, 0x00, 0x63, 0xa0, 0x2c, 0x00, 0x65, 0xaa, + 0x2c, 0x00, 0x5e, 0xa4, 0x2d, 0x00, 0x50, 0x96, 0x26, 0x00, 0xa4, 0xcf, 0x98, 0x00, 0xd9, 0xea, 0xdd, 0x00, 0xb9, + 0xdd, 0xff, 0x00, 0x38, 0x9e, 0xf4, 0x00, 0x00, 0x8f, 0xd4, 0x00, 0x46, 0xc1, 0xef, 0x00, 0x44, 0xc0, 0xef, 0x00, + 0x43, 0xbf, 0xef, 0x00, 0x42, 0xbe, 0xef, 0x00, 0x40, 0xbd, 0xef, 0x00, 0x42, 0xbc, 0xed, 0x00, 0x43, 0xba, 0xec, + 0x00, 0x40, 0xba, 0xf0, 0x00, 0x3d, 0xba, 0xf4, 0x00, 0x35, 0xb8, 0xe7, 0x00, 0x17, 0xbd, 0xf7, 0x00, 0xd9, 0x7f, + 0x50, 0x00, 0xf7, 0x91, 0x47, 0x00, 0xf7, 0xa5, 0x54, 0x00, 0xff, 0xdb, 0xba, 0x00, 0xf8, 0xa2, 0x4d, 0x00, 0xf3, + 0xa5, 0x49, 0x00, 0xf5, 0xad, 0x53, 0x00, 0xf7, 0xb5, 0x5e, 0x00, 0xf9, 0xc1, 0x6f, 0x00, 0xfb, 0xcc, 0x7f, 0x00, + 0xf9, 0xd8, 0x8a, 0x00, 0xf8, 0xe5, 0x95, 0x00, 0xf8, 0xed, 0xa2, 0x00, 0xf8, 0xf5, 0xae, 0x00, 0xff, 0xf3, 0xb2, + 0x00, 0xff, 0xf2, 0xb6, 0x00, 0xfe, 0xf5, 0xae, 0x00, 0xf4, 0xb6, 0x59, 0x00, 0xf9, 0xdb, 0x93, 0x00, 0xfe, 0xff, + 0xcd, 0x00, 0xfb, 0xf6, 0xc1, 0x00, 0xf7, 0xed, 0xb6, 0x00, 0xff, 0xf2, 0xac, 0x00, 0x13, 0xa4, 0xf7, 0x00, 0x16, + 0xa5, 0xf0, 0x00, 0x18, 0xa5, 0xe8, 0x00, 0x56, 0xb4, 0xcd, 0x00, 0xf1, 0xf2, 0x71, 0x00, 0xd5, 0xef, 0x84, 0x00, + 0xcf, 0xe6, 0x7b, 0x00, 0xcd, 0xe7, 0x7c, 0x00, 0xcb, 0xe7, 0x7c, 0x00, 0xc9, 0xe6, 0x72, 0x00, 0xc7, 0xe5, 0x67, + 0x00, 0xbc, 0xe1, 0x5f, 0x00, 0xb1, 0xdd, 0x57, 0x00, 0xa9, 0xdc, 0x51, 0x00, 0xa0, 0xda, 0x4b, 0x00, 0x9d, 0xd7, + 0x49, 0x00, 0x9a, 0xd4, 0x47, 0x00, 0x94, 0xcf, 0x43, 0x00, 0x8f, 0xcb, 0x3f, 0x00, 0x88, 0xc4, 0x3c, 0x00, 0x82, + 0xbe, 0x39, 0x00, 0x72, 0xb4, 0x30, 0x00, 0x63, 0xa9, 0x28, 0x00, 0x59, 0xa0, 0x28, 0x00, 0x4e, 0x98, 0x27, 0x00, + 0xa0, 0xc4, 0x79, 0x00, 0xff, 0xfb, 0xf7, 0x00, 0x7f, 0xd3, 0xf5, 0x00, 0x03, 0x8f, 0xe2, 0x00, 0x0e, 0x89, 0xe2, + 0x00, 0x48, 0xc3, 0xef, 0x00, 0x46, 0xc2, 0xef, 0x00, 0x45, 0xc1, 0xf0, 0x00, 0x43, 0xc0, 0xf0, 0x00, 0x42, 0xbf, + 0xf0, 0x00, 0x42, 0xbe, 0xee, 0x00, 0x43, 0xbd, 0xec, 0x00, 0x41, 0xbc, 0xef, 0x00, 0x3f, 0xbc, 0xf2, 0x00, 0x2f, + 0xc0, 0xfe, 0x00, 0x36, 0xbd, 0xfc, 0x00, 0xf5, 0x4c, 0x00, 0x00, 0xff, 0x8a, 0x52, 0x00, 0xfa, 0xa6, 0x5e, 0x00, + 0xfd, 0xc4, 0x8e, 0x00, 0xfb, 0xc1, 0x85, 0x00, 0xf5, 0xae, 0x50, 0x00, 0xf7, 0xb6, 0x5e, 0x00, 0xf9, 0xbe, 0x6c, + 0x00, 0xfa, 0xc9, 0x78, 0x00, 0xfb, 0xd4, 0x85, 0x00, 0xfe, 0xde, 0x98, 0x00, 0xff, 0xe8, 0xaa, 0x00, 0xfd, 0xee, + 0xae, 0x00, 0xf9, 0xf5, 0xb2, 0x00, 0xfc, 0xf6, 0xba, 0x00, 0xff, 0xf7, 0xc2, 0x00, 0xfc, 0xf0, 0xb2, 0x00, 0xf7, + 0xcc, 0x6e, 0x00, 0xfb, 0xde, 0x91, 0x00, 0xfd, 0xfc, 0xca, 0x00, 0xff, 0xfb, 0xd1, 0x00, 0xff, 0xfd, 0xc8, 0x00, + 0xca, 0xe4, 0xc8, 0x00, 0x16, 0xa1, 0xf2, 0x00, 0x1d, 0xa4, 0xef, 0x00, 0x12, 0xa1, 0xf1, 0x00, 0x9f, 0xd5, 0xb9, + 0x00, 0xea, 0xf2, 0x8c, 0x00, 0xdc, 0xf0, 0x95, 0x00, 0xd9, 0xeb, 0x90, 0x00, 0xd9, 0xec, 0x93, 0x00, 0xd9, 0xec, + 0x95, 0x00, 0xd6, 0xeb, 0x8c, 0x00, 0xd4, 0xea, 0x83, 0x00, 0xc9, 0xe7, 0x79, 0x00, 0xbf, 0xe3, 0x6f, 0x00, 0xb8, + 0xe3, 0x68, 0x00, 0xb1, 0xe2, 0x62, 0x00, 0xaf, 0xe0, 0x5e, 0x00, 0xad, 0xdf, 0x5a, 0x00, 0xa3, 0xd9, 0x52, 0x00, + 0x99, 0xd4, 0x49, 0x00, 0x8e, 0xcb, 0x41, 0x00, 0x84, 0xc3, 0x3a, 0x00, 0x75, 0xb8, 0x33, 0x00, 0x66, 0xac, 0x2c, + 0x00, 0x5d, 0xa3, 0x29, 0x00, 0x55, 0x99, 0x27, 0x00, 0x4b, 0x94, 0x21, 0x00, 0x24, 0x99, 0xb9, 0x00, 0x15, 0x93, + 0xfe, 0x00, 0x09, 0x93, 0xd8, 0x00, 0x0f, 0x90, 0xd8, 0x00, 0x4a, 0xc5, 0xef, 0x00, 0x48, 0xc4, 0xf0, 0x00, 0x46, + 0xc2, 0xf0, 0x00, 0x45, 0xc1, 0xf1, 0x00, 0x43, 0xc0, 0xf1, 0x00, 0x43, 0xbf, 0xef, 0x00, 0x43, 0xbf, 0xed, 0x00, + 0x42, 0xbe, 0xee, 0x00, 0x41, 0xbd, 0xf0, 0x00, 0x38, 0xbb, 0xf0, 0x00, 0x72, 0xa1, 0xb8, 0x00, 0xff, 0x5d, 0x1e, + 0x00, 0xf9, 0x79, 0x31, 0x00, 0xf5, 0xa1, 0x51, 0x00, 0xf9, 0xad, 0x61, 0x00, 0xfe, 0xe0, 0xbd, 0x00, 0xf8, 0xb7, + 0x58, 0x00, 0xfa, 0xbf, 0x69, 0x00, 0xfc, 0xc8, 0x7a, 0x00, 0xfc, 0xd2, 0x82, 0x00, 0xfc, 0xdc, 0x8b, 0x00, 0xfb, + 0xde, 0x8f, 0x00, 0xfb, 0xe1, 0x93, 0x00, 0xfb, 0xeb, 0xa4, 0x00, 0xfb, 0xf5, 0xb5, 0x00, 0xfa, 0xf8, 0xc2, 0x00, + 0xf9, 0xfc, 0xce, 0x00, 0xf9, 0xec, 0xb7, 0x00, 0xfa, 0xe1, 0x83, 0x00, 0xfe, 0xe2, 0x90, 0x00, 0xfb, 0xfa, 0xc8, + 0x00, 0xfd, 0xf8, 0xd8, 0x00, 0xff, 0xfc, 0xcb, 0x00, 0x8b, 0xce, 0xdc, 0x00, 0x18, 0x9f, 0xee, 0x00, 0x25, 0xa3, + 0xee, 0x00, 0x0b, 0x9d, 0xfb, 0x00, 0xe8, 0xf6, 0xa5, 0x00, 0xe4, 0xf1, 0xa6, 0x00, 0xe4, 0xf0, 0xa6, 0x00, 0xe4, + 0xef, 0xa6, 0x00, 0xe5, 0xf1, 0xaa, 0x00, 0xe6, 0xf2, 0xad, 0x00, 0xe3, 0xf1, 0xa6, 0x00, 0xe0, 0xef, 0x9e, 0x00, + 0xd7, 0xec, 0x93, 0x00, 0xcd, 0xe9, 0x87, 0x00, 0xc8, 0xea, 0x80, 0x00, 0xc2, 0xeb, 0x78, 0x00, 0xc1, 0xea, 0x73, + 0x00, 0xc0, 0xe9, 0x6e, 0x00, 0xb1, 0xe3, 0x60, 0x00, 0xa3, 0xdd, 0x53, 0x00, 0x94, 0xd2, 0x47, 0x00, 0x86, 0xc8, + 0x3b, 0x00, 0x78, 0xbc, 0x35, 0x00, 0x69, 0xb0, 0x30, 0x00, 0x62, 0xa5, 0x2b, 0x00, 0x5b, 0x9b, 0x27, 0x00, 0x57, + 0x92, 0x0a, 0x00, 0x09, 0x95, 0xfc, 0x00, 0x0d, 0x96, 0xe5, 0x00, 0x10, 0x91, 0xeb, 0x00, 0x10, 0x91, 0xeb, 0x00, + 0x4a, 0xc5, 0xf0, 0x00, 0x49, 0xc4, 0xf0, 0x00, 0x47, 0xc3, 0xf1, 0x00, 0x45, 0xc2, 0xf1, 0x00, 0x44, 0xc1, 0xf2, + 0x00, 0x41, 0xc1, 0xf2, 0x00, 0x3f, 0xc1, 0xf2, 0x00, 0x3f, 0xbf, 0xf1, 0x00, 0x3f, 0xbc, 0xf0, 0x00, 0x32, 0xc3, + 0xfe, 0x00, 0xbe, 0x7f, 0x6e, 0x00, 0xfe, 0x65, 0x26, 0x00, 0xf6, 0x7b, 0x35, 0x00, 0xf5, 0x9a, 0x4d, 0x00, 0xf8, + 0xab, 0x5c, 0x00, 0xfb, 0xd0, 0xa0, 0x00, 0xf7, 0xc7, 0x83, 0x00, 0xfe, 0xc1, 0x6b, 0x00, 0xfd, 0xd1, 0x7f, 0x00, + 0xfb, 0xdb, 0x87, 0x00, 0xf9, 0xe5, 0x90, 0x00, 0xf8, 0xed, 0x9a, 0x00, 0xf7, 0xf4, 0xa5, 0x00, 0xfb, 0xea, 0x9a, + 0x00, 0xff, 0xdf, 0x8e, 0x00, 0xfc, 0xe3, 0xa0, 0x00, 0xf7, 0xe6, 0xb1, 0x00, 0xfc, 0xee, 0xcc, 0x00, 0xff, 0xfb, + 0xcb, 0x00, 0xff, 0xf3, 0xc7, 0x00, 0xfc, 0xf1, 0xc3, 0x00, 0xfe, 0xf5, 0xd2, 0x00, 0xff, 0xfc, 0xd3, 0x00, 0x4b, + 0xb5, 0xe7, 0x00, 0x21, 0xa5, 0xed, 0x00, 0x1c, 0xa2, 0xee, 0x00, 0x3d, 0xaa, 0xe2, 0x00, 0xee, 0xf6, 0xac, 0x00, + 0xe6, 0xf2, 0xb1, 0x00, 0xe8, 0xf2, 0xb5, 0x00, 0xe9, 0xf3, 0xb8, 0x00, 0xea, 0xf4, 0xba, 0x00, 0xeb, 0xf5, 0xbc, + 0x00, 0xe8, 0xf3, 0xb6, 0x00, 0xe6, 0xf2, 0xaf, 0x00, 0xe0, 0xf0, 0xa8, 0x00, 0xdb, 0xee, 0xa2, 0x00, 0xd6, 0xef, + 0x9a, 0x00, 0xd1, 0xf0, 0x92, 0x00, 0xc9, 0xed, 0x82, 0x00, 0xc1, 0xeb, 0x73, 0x00, 0xb0, 0xe3, 0x62, 0x00, 0xa1, + 0xdc, 0x51, 0x00, 0x94, 0xd3, 0x47, 0x00, 0x88, 0xca, 0x3e, 0x00, 0x7b, 0xbf, 0x38, 0x00, 0x6e, 0xb4, 0x33, 0x00, + 0x66, 0xa9, 0x2e, 0x00, 0x5d, 0xa0, 0x1b, 0x00, 0x3d, 0x94, 0x48, 0x00, 0x0a, 0x93, 0xf6, 0x00, 0x0e, 0x94, 0xec, + 0x00, 0x11, 0x93, 0xf0, 0x00, 0x11, 0x93, 0xf0, 0x00, 0x4b, 0xc5, 0xf1, 0x00, 0x4a, 0xc5, 0xf1, 0x00, 0x48, 0xc4, + 0xf1, 0x00, 0x47, 0xc3, 0xf2, 0x00, 0x45, 0xc3, 0xf2, 0x00, 0x40, 0xc3, 0xf4, 0x00, 0x3b, 0xc4, 0xf6, 0x00, 0x3c, + 0xbf, 0xf3, 0x00, 0x3e, 0xbb, 0xf0, 0x00, 0x2d, 0xca, 0xff, 0x00, 0xff, 0x5d, 0x25, 0x00, 0xfe, 0x6d, 0x2f, 0x00, + 0xf3, 0x7d, 0x39, 0x00, 0xf5, 0x93, 0x48, 0x00, 0xf8, 0xa9, 0x58, 0x00, 0xf7, 0xc0, 0x83, 0x00, 0xf7, 0xd7, 0xae, + 0x00, 0xff, 0xc3, 0x6d, 0x00, 0xff, 0xda, 0x84, 0x00, 0xfb, 0xe4, 0x8c, 0x00, 0xf7, 0xee, 0x94, 0x00, 0xf8, 0xed, + 0x9e, 0x00, 0xfa, 0xec, 0xa7, 0x00, 0xf9, 0xf1, 0xb4, 0x00, 0xf8, 0xf6, 0xc1, 0x00, 0xfc, 0xf6, 0xc8, 0x00, 0xff, + 0xf6, 0xd0, 0x00, 0xfe, 0xf2, 0xd3, 0x00, 0xfc, 0xf4, 0xba, 0x00, 0xff, 0xfe, 0xe8, 0x00, 0xf7, 0xfd, 0xea, 0x00, + 0xfd, 0xfd, 0xe3, 0x00, 0xff, 0xfc, 0xdc, 0x00, 0x0b, 0x9d, 0xf1, 0x00, 0x2a, 0xaa, 0xed, 0x00, 0x1b, 0xaa, 0xf6, + 0x00, 0x80, 0xc8, 0xda, 0x00, 0xfd, 0xff, 0xbb, 0x00, 0xe8, 0xf2, 0xbd, 0x00, 0xeb, 0xf4, 0xc4, 0x00, 0xef, 0xf7, + 0xcb, 0x00, 0xef, 0xf7, 0xcb, 0x00, 0xef, 0xf7, 0xcb, 0x00, 0xed, 0xf6, 0xc5, 0x00, 0xeb, 0xf5, 0xc0, 0x00, 0xea, + 0xf4, 0xbe, 0x00, 0xe8, 0xf3, 0xbd, 0x00, 0xe4, 0xf4, 0xb4, 0x00, 0xe0, 0xf6, 0xab, 0x00, 0xd0, 0xf1, 0x91, 0x00, + 0xc1, 0xec, 0x77, 0x00, 0xb0, 0xe4, 0x63, 0x00, 0x9e, 0xdb, 0x4e, 0x00, 0x95, 0xd4, 0x48, 0x00, 0x8b, 0xcc, 0x42, + 0x00, 0x7f, 0xc2, 0x3b, 0x00, 0x73, 0xb9, 0x35, 0x00, 0x6a, 0xac, 0x31, 0x00, 0x60, 0xa5, 0x10, 0x00, 0x22, 0x96, + 0x87, 0x00, 0x0b, 0x91, 0xf1, 0x00, 0x0e, 0x93, 0xf3, 0x00, 0x12, 0x94, 0xf5, 0x00, 0x12, 0x94, 0xf5, 0x00, 0x4c, + 0xc6, 0xf1, 0x00, 0x4b, 0xc5, 0xf2, 0x00, 0x49, 0xc5, 0xf2, 0x00, 0x47, 0xc4, 0xf2, 0x00, 0x46, 0xc4, 0xf2, 0x00, + 0x43, 0xc4, 0xf1, 0x00, 0x40, 0xc4, 0xf0, 0x00, 0x42, 0xc0, 0xf3, 0x00, 0x39, 0xc1, 0xf6, 0x00, 0x5e, 0xac, 0xca, + 0x00, 0xfb, 0x59, 0x1e, 0x00, 0xf3, 0x6e, 0x31, 0x00, 0xf8, 0x81, 0x35, 0x00, 0xfb, 0x92, 0x3f, 0x00, 0xfb, 0xaf, + 0x5e, 0x00, 0xff, 0xc3, 0x73, 0x00, 0xfd, 0xe2, 0xba, 0x00, 0xff, 0xcd, 0x75, 0x00, 0xff, 0xd3, 0x72, 0x00, 0xff, + 0xe5, 0x84, 0x00, 0xff, 0xf7, 0x96, 0x00, 0xfe, 0xf4, 0xa2, 0x00, 0xfd, 0xf1, 0xae, 0x00, 0xff, 0xf8, 0xc2, 0x00, + 0xfc, 0xf8, 0xcd, 0x00, 0xfe, 0xf8, 0xd2, 0x00, 0xff, 0xf9, 0xd6, 0x00, 0xfe, 0xf6, 0xe1, 0x00, 0xfc, 0xf5, 0xdd, + 0x00, 0xff, 0xfb, 0xee, 0x00, 0xfb, 0xfc, 0xe8, 0x00, 0xff, 0xfc, 0xe0, 0x00, 0xb2, 0xe0, 0xe8, 0x00, 0x19, 0xa4, + 0xf0, 0x00, 0x26, 0xab, 0xec, 0x00, 0x16, 0xa8, 0xf6, 0x00, 0xc2, 0xe4, 0xd8, 0x00, 0xf9, 0xfa, 0xc5, 0x00, 0xef, + 0xf6, 0xcb, 0x00, 0xf0, 0xf7, 0xce, 0x00, 0xf1, 0xf8, 0xd2, 0x00, 0xf1, 0xf8, 0xd1, 0x00, 0xf2, 0xf9, 0xd1, 0x00, + 0xf1, 0xf9, 0xcd, 0x00, 0xf1, 0xf9, 0xca, 0x00, 0xf2, 0xfb, 0xca, 0x00, 0xf4, 0xfd, 0xca, 0x00, 0xe7, 0xf8, 0xb6, + 0x00, 0xda, 0xf3, 0xa2, 0x00, 0xcb, 0xef, 0x8a, 0x00, 0xbc, 0xec, 0x71, 0x00, 0xb0, 0xe6, 0x61, 0x00, 0xa5, 0xe1, + 0x51, 0x00, 0x9a, 0xd9, 0x49, 0x00, 0x8f, 0xd2, 0x40, 0x00, 0x83, 0xc7, 0x3b, 0x00, 0x77, 0xbc, 0x35, 0x00, 0x6a, + 0xb3, 0x1d, 0x00, 0x5e, 0xa9, 0x05, 0x00, 0x13, 0x8d, 0xea, 0x00, 0x11, 0x93, 0xef, 0x00, 0x10, 0x93, 0xf0, 0x00, + 0x0f, 0x93, 0xf0, 0x00, 0x0f, 0x93, 0xf0, 0x00, 0x4d, 0xc6, 0xf2, 0x00, 0x4c, 0xc6, 0xf2, 0x00, 0x4a, 0xc5, 0xf3, + 0x00, 0x48, 0xc5, 0xf3, 0x00, 0x47, 0xc5, 0xf3, 0x00, 0x46, 0xc4, 0xef, 0x00, 0x46, 0xc4, 0xeb, 0x00, 0x48, 0xc0, + 0xf3, 0x00, 0x34, 0xc7, 0xfb, 0x00, 0x98, 0x95, 0x91, 0x00, 0xfc, 0x64, 0x28, 0x00, 0xf1, 0x77, 0x3b, 0x00, 0xfc, + 0x84, 0x32, 0x00, 0xff, 0x91, 0x35, 0x00, 0xff, 0xb5, 0x64, 0x00, 0xff, 0xbe, 0x5a, 0x00, 0xf3, 0xdd, 0xb6, 0x00, + 0xcc, 0xd0, 0x97, 0x00, 0xb4, 0xce, 0xa5, 0x00, 0xb0, 0xd3, 0xb1, 0x00, 0xab, 0xd7, 0xbd, 0x00, 0xc3, 0xe1, 0xbf, + 0x00, 0xda, 0xeb, 0xc1, 0x00, 0xf5, 0xfd, 0xc7, 0x00, 0xff, 0xff, 0xbd, 0x00, 0xff, 0xfe, 0xcd, 0x00, 0xff, 0xfc, + 0xdc, 0x00, 0xff, 0xfc, 0xe0, 0x00, 0xfb, 0xfc, 0xe5, 0x00, 0xfd, 0xfb, 0xe6, 0x00, 0xff, 0xfa, 0xe7, 0x00, 0xff, + 0xfb, 0xdd, 0x00, 0x61, 0xc4, 0xf4, 0x00, 0x26, 0xaa, 0xee, 0x00, 0x22, 0xab, 0xec, 0x00, 0x10, 0xa7, 0xf6, 0x00, + 0xff, 0xff, 0xd7, 0x00, 0xf5, 0xf5, 0xd0, 0x00, 0xf6, 0xfa, 0xd9, 0x00, 0xf4, 0xf9, 0xd9, 0x00, 0xf2, 0xf9, 0xda, + 0x00, 0xf3, 0xfa, 0xd8, 0x00, 0xf4, 0xfb, 0xd7, 0x00, 0xf5, 0xfc, 0xd5, 0x00, 0xf7, 0xfd, 0xd4, 0x00, 0xf3, 0xfa, + 0xce, 0x00, 0xf0, 0xf7, 0xc8, 0x00, 0xe2, 0xf4, 0xb0, 0x00, 0xd4, 0xf1, 0x99, 0x00, 0xc5, 0xee, 0x82, 0x00, 0xb7, + 0xeb, 0x6b, 0x00, 0xb1, 0xe9, 0x5f, 0x00, 0xab, 0xe7, 0x54, 0x00, 0x9f, 0xdf, 0x49, 0x00, 0x94, 0xd8, 0x3f, 0x00, + 0x87, 0xcc, 0x3a, 0x00, 0x7b, 0xc0, 0x34, 0x00, 0x6b, 0xb4, 0x25, 0x00, 0x5b, 0xa3, 0x32, 0x00, 0x04, 0x95, 0xf9, + 0x00, 0x17, 0x95, 0xee, 0x00, 0x12, 0x93, 0xed, 0x00, 0x0c, 0x91, 0xeb, 0x00, 0x0c, 0x91, 0xeb, 0x00, 0x4f, 0xc8, + 0xf3, 0x00, 0x4d, 0xc8, 0xf3, 0x00, 0x4c, 0xc8, 0xf4, 0x00, 0x4b, 0xc8, 0xf4, 0x00, 0x49, 0xc8, 0xf4, 0x00, 0x47, + 0xc5, 0xf2, 0x00, 0x45, 0xc2, 0xef, 0x00, 0x42, 0xc2, 0xf8, 0x00, 0x34, 0xc8, 0xff, 0x00, 0xdf, 0x67, 0x46, 0x00, + 0xff, 0x63, 0x2a, 0x00, 0xff, 0x70, 0x1b, 0x00, 0xe1, 0x8b, 0x53, 0x00, 0xa4, 0xa1, 0x85, 0x00, 0x63, 0xc1, 0xcd, + 0x00, 0x26, 0xc0, 0xff, 0x00, 0x2a, 0xb8, 0xff, 0x00, 0x25, 0xb5, 0xf1, 0x00, 0x27, 0xb7, 0xf9, 0x00, 0x26, 0xb5, + 0xf6, 0x00, 0x23, 0xb3, 0xf2, 0x00, 0x24, 0xb5, 0xfa, 0x00, 0x25, 0xb7, 0xff, 0x00, 0x18, 0x9d, 0xdf, 0x00, 0x43, + 0xbb, 0xf4, 0x00, 0x9e, 0xda, 0xe8, 0x00, 0xf9, 0xf9, 0xdc, 0x00, 0xf3, 0xfb, 0xe6, 0x00, 0xff, 0xff, 0xea, 0x00, + 0xfd, 0xff, 0xe6, 0x00, 0xfa, 0xfc, 0xe2, 0x00, 0xff, 0xff, 0xff, 0x00, 0x1e, 0xa8, 0xef, 0x00, 0x1c, 0xa8, 0xf1, + 0x00, 0x1b, 0xa8, 0xf2, 0x00, 0x5b, 0xc4, 0xf1, 0x00, 0xff, 0xff, 0xe7, 0x00, 0xfb, 0xf9, 0xe1, 0x00, 0xfb, 0xfc, + 0xe3, 0x00, 0xf8, 0xfb, 0xe0, 0x00, 0xf5, 0xfa, 0xdd, 0x00, 0xf5, 0xfb, 0xdb, 0x00, 0xf5, 0xfb, 0xda, 0x00, 0xf6, + 0xfc, 0xd7, 0x00, 0xf6, 0xfd, 0xd3, 0x00, 0xf0, 0xf8, 0xc9, 0x00, 0xeb, 0xf4, 0xbe, 0x00, 0xdf, 0xf2, 0xa9, 0x00, + 0xd4, 0xf0, 0x94, 0x00, 0xc7, 0xf4, 0x7b, 0x00, 0xba, 0xf8, 0x62, 0x00, 0xb0, 0xef, 0x58, 0x00, 0xa6, 0xe6, 0x4e, + 0x00, 0xa3, 0xe2, 0x48, 0x00, 0x98, 0xd7, 0x3a, 0x00, 0x8a, 0xcd, 0x38, 0x00, 0x7b, 0xc4, 0x35, 0x00, 0x70, 0xb8, + 0x21, 0x00, 0x3b, 0x9c, 0x84, 0x00, 0x0d, 0x93, 0xf4, 0x00, 0x13, 0x94, 0xed, 0x00, 0x11, 0x93, 0xe9, 0x00, 0x0f, + 0x92, 0xe6, 0x00, 0x0f, 0x92, 0xe6, 0x00, 0x50, 0xc9, 0xf4, 0x00, 0x4f, 0xca, 0xf4, 0x00, 0x4e, 0xca, 0xf5, 0x00, + 0x4d, 0xca, 0xf5, 0x00, 0x4c, 0xca, 0xf6, 0x00, 0x48, 0xc5, 0xf4, 0x00, 0x45, 0xc0, 0xf3, 0x00, 0x47, 0xc2, 0xef, + 0x00, 0x4a, 0xc4, 0xeb, 0x00, 0xff, 0x52, 0x1f, 0x00, 0xa7, 0x9a, 0x92, 0x00, 0x51, 0xb7, 0xe6, 0x00, 0x28, 0xc7, + 0xff, 0x00, 0x2c, 0xc4, 0xf9, 0x00, 0x31, 0xc1, 0xf1, 0x00, 0x3f, 0xbb, 0xf0, 0x00, 0x37, 0xc0, 0xef, 0x00, 0x39, + 0xb9, 0xf0, 0x00, 0x3b, 0xb3, 0xf1, 0x00, 0x38, 0xb5, 0xf4, 0x00, 0x36, 0xb7, 0xf7, 0x00, 0x32, 0xb9, 0xf0, 0x00, + 0x2f, 0xbb, 0xe8, 0x00, 0x2f, 0xb8, 0xeb, 0x00, 0x2f, 0xb5, 0xed, 0x00, 0x20, 0xac, 0xf3, 0x00, 0x10, 0xa3, 0xfa, + 0x00, 0x70, 0xc9, 0xf3, 0x00, 0xf5, 0xf9, 0xdf, 0x00, 0xf6, 0xfb, 0xde, 0x00, 0xf6, 0xfd, 0xde, 0x00, 0xd8, 0xeb, + 0xe4, 0x00, 0x11, 0xa5, 0xee, 0x00, 0x2d, 0xb2, 0xf5, 0x00, 0x14, 0xa5, 0xf8, 0x00, 0xa5, 0xe2, 0xec, 0x00, 0xff, + 0xff, 0xf8, 0x00, 0xff, 0xfe, 0xf3, 0x00, 0xff, 0xfd, 0xed, 0x00, 0xfc, 0xfd, 0xe6, 0x00, 0xf8, 0xfc, 0xe0, 0x00, + 0xf7, 0xfc, 0xde, 0x00, 0xf6, 0xfc, 0xdd, 0x00, 0xf6, 0xfc, 0xd8, 0x00, 0xf5, 0xfd, 0xd3, 0x00, 0xed, 0xf7, 0xc4, + 0x00, 0xe5, 0xf1, 0xb4, 0x00, 0xe5, 0xf5, 0xb8, 0x00, 0xe4, 0xf9, 0xbb, 0x00, 0xec, 0xfe, 0xd2, 0x00, 0xf3, 0xff, + 0xe9, 0x00, 0xed, 0xfe, 0xdb, 0x00, 0xe8, 0xf9, 0xcd, 0x00, 0xca, 0xef, 0x89, 0x00, 0x9c, 0xd6, 0x36, 0x00, 0x84, + 0xc7, 0x2e, 0x00, 0x6b, 0xb8, 0x26, 0x00, 0x6c, 0xb3, 0x15, 0x00, 0x1a, 0x95, 0xd6, 0x00, 0x15, 0x91, 0xef, 0x00, + 0x10, 0x93, 0xeb, 0x00, 0x11, 0x93, 0xe6, 0x00, 0x12, 0x94, 0xe1, 0x00, 0x12, 0x94, 0xe1, 0x00, 0x52, 0xcb, 0xf4, + 0x00, 0x50, 0xca, 0xf4, 0x00, 0x4e, 0xca, 0xf4, 0x00, 0x4c, 0xca, 0xf3, 0x00, 0x4a, 0xc9, 0xf3, 0x00, 0x48, 0xc8, + 0xf5, 0x00, 0x46, 0xc7, 0xf6, 0x00, 0x40, 0xbf, 0xed, 0x00, 0x41, 0xbf, 0xeb, 0x00, 0x41, 0xd4, 0xf9, 0x00, 0x33, + 0xc9, 0xfc, 0x00, 0x2f, 0xc9, 0xff, 0x00, 0x42, 0xc3, 0xec, 0x00, 0x40, 0xc3, 0xf4, 0x00, 0x3e, 0xc3, 0xfc, 0x00, + 0x35, 0xbb, 0xf4, 0x00, 0x33, 0xbb, 0xf3, 0x00, 0x49, 0xbd, 0xf7, 0x00, 0x39, 0xb7, 0xf9, 0x00, 0x37, 0xb7, 0xf6, + 0x00, 0x35, 0xb7, 0xf2, 0x00, 0x2e, 0xb5, 0xf4, 0x00, 0x28, 0xb3, 0xf5, 0x00, 0x2f, 0xbb, 0xf8, 0x00, 0x2f, 0xba, + 0xf2, 0x00, 0x30, 0xb5, 0xf2, 0x00, 0x31, 0xb0, 0xf1, 0x00, 0x1f, 0xac, 0xf6, 0x00, 0x0d, 0xab, 0xed, 0x00, 0x7f, + 0xd2, 0xed, 0x00, 0xff, 0xff, 0xe6, 0x00, 0x80, 0xd9, 0xd2, 0x00, 0x2f, 0xaa, 0xf8, 0x00, 0x1d, 0xaf, 0xec, 0x00, + 0x03, 0xaa, 0xe6, 0x00, 0xff, 0xf8, 0xff, 0x00, 0xff, 0xff, 0xfe, 0x00, 0xff, 0xff, 0xf9, 0x00, 0xff, 0xfd, 0xf4, + 0x00, 0xfd, 0xfe, 0xeb, 0x00, 0xfb, 0xfe, 0xe3, 0x00, 0xf9, 0xfd, 0xe1, 0x00, 0xf7, 0xfc, 0xe0, 0x00, 0xf5, 0xfd, + 0xd8, 0x00, 0xf4, 0xfd, 0xcf, 0x00, 0xf5, 0xfc, 0xe2, 0x00, 0xf6, 0xfd, 0xe8, 0x00, 0xf3, 0xfd, 0xe8, 0x00, 0xf1, + 0xfd, 0xe9, 0x00, 0xeb, 0xfd, 0xd3, 0x00, 0xe6, 0xfd, 0xbe, 0x00, 0xe0, 0xf8, 0xba, 0x00, 0xda, 0xf2, 0xb7, 0x00, + 0xea, 0xfc, 0xd2, 0x00, 0xf2, 0xfd, 0xe6, 0x00, 0xb7, 0xde, 0x8d, 0x00, 0x84, 0xc7, 0x3d, 0x00, 0x9a, 0xb8, 0x48, + 0x00, 0x14, 0xa1, 0xf9, 0x00, 0x04, 0x94, 0xf3, 0x00, 0x10, 0x94, 0xef, 0x00, 0x10, 0x95, 0xec, 0x00, 0x10, 0x95, + 0xe9, 0x00, 0x10, 0x95, 0xe9, 0x00, 0x54, 0xcc, 0xf5, 0x00, 0x51, 0xcb, 0xf4, 0x00, 0x4e, 0xca, 0xf3, 0x00, 0x4c, + 0xc9, 0xf2, 0x00, 0x49, 0xc8, 0xf1, 0x00, 0x48, 0xcb, 0xf5, 0x00, 0x48, 0xce, 0xf9, 0x00, 0x40, 0xc4, 0xf3, 0x00, + 0x49, 0xca, 0xfc, 0x00, 0x40, 0xc2, 0xf1, 0x00, 0x47, 0xca, 0xf5, 0x00, 0x46, 0xc7, 0xf4, 0x00, 0x46, 0xc4, 0xf3, + 0x00, 0x39, 0xb5, 0xee, 0x00, 0x2c, 0xa5, 0xe8, 0x00, 0x2e, 0xb1, 0xe1, 0x00, 0x56, 0xc1, 0xea, 0x00, 0x6d, 0xc9, + 0xe9, 0x00, 0x37, 0xc2, 0xe5, 0x00, 0x51, 0xca, 0xeb, 0x00, 0x6b, 0xd2, 0xf1, 0x00, 0x74, 0xd1, 0xf5, 0x00, 0x7d, + 0xcf, 0xf9, 0x00, 0x56, 0xc7, 0xf8, 0x00, 0x1f, 0xaf, 0xe8, 0x00, 0x25, 0xb1, 0xee, 0x00, 0x2c, 0xb3, 0xf4, 0x00, + 0x3e, 0xb5, 0xf9, 0x00, 0x2b, 0xb3, 0xee, 0x00, 0x1b, 0xaf, 0xf5, 0x00, 0x32, 0xb5, 0xf0, 0x00, 0x3f, 0xb2, 0xf9, + 0x00, 0x26, 0xa9, 0xf2, 0x00, 0x1f, 0xae, 0xeb, 0x00, 0x3f, 0xb8, 0xf4, 0x00, 0xfc, 0xff, 0xf3, 0x00, 0xff, 0xff, + 0xff, 0x00, 0xff, 0xff, 0xff, 0x00, 0xff, 0xfe, 0xfb, 0x00, 0xfe, 0xff, 0xf1, 0x00, 0xfe, 0xff, 0xe6, 0x00, 0xfb, + 0xff, 0xe5, 0x00, 0xf8, 0xfd, 0xe3, 0x00, 0xf5, 0xfd, 0xd7, 0x00, 0xf3, 0xfe, 0xcb, 0x00, 0xf5, 0xfb, 0xeb, 0x00, + 0xf7, 0xfe, 0xee, 0x00, 0xf2, 0xfd, 0xde, 0x00, 0xed, 0xfc, 0xcf, 0x00, 0xe3, 0xf9, 0xb0, 0x00, 0xd9, 0xf6, 0x92, + 0x00, 0xd2, 0xf4, 0x8b, 0x00, 0xcc, 0xf1, 0x84, 0x00, 0xce, 0xee, 0x97, 0x00, 0xd0, 0xea, 0xa9, 0x00, 0xda, 0xeb, + 0xc1, 0x00, 0xf4, 0xfb, 0xe9, 0x00, 0x7f, 0xc6, 0x79, 0x00, 0x5a, 0xc1, 0xff, 0x00, 0x1a, 0xa1, 0xeb, 0x00, 0x11, + 0x95, 0xf2, 0x00, 0x0f, 0x96, 0xf2, 0x00, 0x0e, 0x97, 0xf2, 0x00, 0x0e, 0x97, 0xf2, 0x00, 0x54, 0xcd, 0xf5, 0x00, + 0x52, 0xcc, 0xf4, 0x00, 0x4f, 0xcb, 0xf3, 0x00, 0x4d, 0xc9, 0xf3, 0x00, 0x4a, 0xc8, 0xf2, 0x00, 0x49, 0xc6, 0xf2, + 0x00, 0x47, 0xc4, 0xf2, 0x00, 0x49, 0xd2, 0xf3, 0x00, 0x46, 0xc8, 0xf3, 0x00, 0x4d, 0xc5, 0xfc, 0x00, 0x2c, 0x9a, + 0xdd, 0x00, 0x18, 0x83, 0xcd, 0x00, 0x04, 0x6c, 0xbe, 0x00, 0x00, 0x80, 0xc5, 0x00, 0x0f, 0x96, 0xd4, 0x00, 0x2e, + 0xad, 0xdb, 0x00, 0x60, 0xc6, 0xeb, 0x00, 0x76, 0xcd, 0xef, 0x00, 0x51, 0xca, 0xea, 0x00, 0x69, 0xd2, 0xf0, 0x00, + 0x81, 0xda, 0xf5, 0x00, 0x9a, 0xe4, 0xf7, 0x00, 0xb3, 0xef, 0xf9, 0x00, 0xcf, 0xfa, 0xff, 0x00, 0xe3, 0xfe, 0xff, + 0x00, 0x9a, 0xe1, 0xff, 0x00, 0x48, 0xbc, 0xf7, 0x00, 0x11, 0xb5, 0xdd, 0x00, 0x32, 0xae, 0xf0, 0x00, 0x28, 0xac, + 0xfc, 0x00, 0x31, 0xb2, 0xf3, 0x00, 0x34, 0xb1, 0xf6, 0x00, 0x25, 0xad, 0xf0, 0x00, 0x26, 0xac, 0xf6, 0x00, 0x98, + 0xd1, 0xfc, 0x00, 0xff, 0xfd, 0xf8, 0x00, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xfb, 0x00, 0xfe, 0xff, 0xf4, 0x00, + 0xfd, 0xff, 0xee, 0x00, 0xfc, 0xfd, 0xe7, 0x00, 0xfb, 0xfe, 0xe4, 0x00, 0xfa, 0xff, 0xe0, 0x00, 0xf8, 0xfd, 0xe7, + 0x00, 0xf7, 0xfc, 0xef, 0x00, 0xf3, 0xfb, 0xeb, 0x00, 0xef, 0xfd, 0xd9, 0x00, 0xe9, 0xfb, 0xc2, 0x00, 0xe3, 0xf9, + 0xac, 0x00, 0xd9, 0xf4, 0x9b, 0x00, 0xce, 0xef, 0x8b, 0x00, 0xc1, 0xea, 0x76, 0x00, 0xb4, 0xe5, 0x62, 0x00, 0xab, + 0xdd, 0x5a, 0x00, 0xa2, 0xd2, 0x61, 0x00, 0xc1, 0xe9, 0x8e, 0x00, 0xdb, 0xe8, 0xb9, 0x00, 0x96, 0xd4, 0xff, 0x00, + 0x8e, 0xd0, 0xfa, 0x00, 0x42, 0xae, 0xee, 0x00, 0x10, 0x95, 0xf1, 0x00, 0x10, 0x96, 0xf1, 0x00, 0x0f, 0x96, 0xf1, + 0x00, 0x0f, 0x96, 0xf1, 0x00, 0x55, 0xce, 0xf5, 0x00, 0x53, 0xcc, 0xf4, 0x00, 0x50, 0xcb, 0xf4, 0x00, 0x4e, 0xca, + 0xf4, 0x00, 0x4c, 0xc8, 0xf4, 0x00, 0x51, 0xca, 0xf7, 0x00, 0x57, 0xcb, 0xfa, 0x00, 0x45, 0xc0, 0xea, 0x00, 0x1a, + 0x75, 0xc7, 0x00, 0x00, 0x58, 0xad, 0x00, 0x01, 0x5b, 0xb4, 0x00, 0x06, 0x6f, 0xc0, 0x00, 0x0b, 0x84, 0xcd, 0x00, + 0x00, 0x93, 0xce, 0x00, 0x11, 0xa7, 0xe0, 0x00, 0x3e, 0xb9, 0xe6, 0x00, 0x6b, 0xcb, 0xeb, 0x00, 0x7e, 0xd1, 0xf6, + 0x00, 0x6c, 0xd3, 0xf0, 0x00, 0x82, 0xdb, 0xf4, 0x00, 0x98, 0xe3, 0xf9, 0x00, 0xa5, 0xec, 0xf7, 0x00, 0xb2, 0xf4, + 0xf5, 0x00, 0xc7, 0xf7, 0xf9, 0x00, 0xdd, 0xfa, 0xfd, 0x00, 0xf2, 0xff, 0xff, 0x00, 0xf8, 0xff, 0xf6, 0x00, 0xbc, + 0xeb, 0xfe, 0x00, 0x22, 0xb4, 0xf2, 0x00, 0x29, 0xaf, 0xff, 0x00, 0x2f, 0xb0, 0xf7, 0x00, 0x29, 0xb1, 0xf2, 0x00, + 0x23, 0xb1, 0xee, 0x00, 0x1a, 0xa7, 0xfa, 0x00, 0xca, 0xe6, 0xf4, 0x00, 0xf7, 0xf8, 0xf4, 0x00, 0xfe, 0xff, 0xff, + 0x00, 0xfe, 0xff, 0xf7, 0x00, 0xfe, 0xff, 0xed, 0x00, 0xfc, 0xff, 0xeb, 0x00, 0xfb, 0xfa, 0xe9, 0x00, 0xfb, 0xfe, + 0xe3, 0x00, 0xfb, 0xff, 0xdc, 0x00, 0xfb, 0xff, 0xe9, 0x00, 0xfb, 0xff, 0xf7, 0x00, 0xf1, 0xfe, 0xdd, 0x00, 0xe7, + 0xfb, 0xc3, 0x00, 0xe0, 0xf6, 0xb4, 0x00, 0xd8, 0xf0, 0xa5, 0x00, 0xce, 0xec, 0x94, 0x00, 0xc4, 0xe8, 0x84, 0x00, + 0xb8, 0xe6, 0x78, 0x00, 0xac, 0xe3, 0x6c, 0x00, 0xa0, 0xdf, 0x53, 0x00, 0x94, 0xd4, 0x55, 0x00, 0x80, 0xbd, 0x41, + 0x00, 0xd2, 0xe5, 0x99, 0x00, 0x2c, 0xa1, 0xf4, 0x00, 0x30, 0xa2, 0xf6, 0x00, 0x20, 0x9c, 0xf3, 0x00, 0x10, 0x96, + 0xf1, 0x00, 0x10, 0x96, 0xf1, 0x00, 0x10, 0x96, 0xf1, 0x00, 0x10, 0x96, 0xf1, 0x00, 0x55, 0xce, 0xf4, 0x00, 0x53, + 0xcd, 0xf4, 0x00, 0x51, 0xcb, 0xf5, 0x00, 0x50, 0xcb, 0xf5, 0x00, 0x4e, 0xca, 0xf6, 0x00, 0x4d, 0xc9, 0xf4, 0x00, + 0x54, 0xd0, 0xfa, 0x00, 0x2b, 0x86, 0xce, 0x00, 0x07, 0x52, 0xb1, 0x00, 0x04, 0x5f, 0xb9, 0x00, 0x0a, 0x74, 0xc9, + 0x00, 0x08, 0x82, 0xce, 0x00, 0x06, 0x91, 0xd4, 0x00, 0x02, 0xa0, 0xd5, 0x00, 0x24, 0xb5, 0xe7, 0x00, 0x4c, 0xc4, + 0xea, 0x00, 0x74, 0xd3, 0xee, 0x00, 0x83, 0xd9, 0xf5, 0x00, 0x7f, 0xdd, 0xf4, 0x00, 0x93, 0xe4, 0xf6, 0x00, 0xa8, + 0xec, 0xf9, 0x00, 0xb6, 0xf2, 0xf9, 0x00, 0xc3, 0xf9, 0xf9, 0x00, 0xd3, 0xfa, 0xfb, 0x00, 0xe3, 0xfc, 0xfc, 0x00, + 0xed, 0xfe, 0xfb, 0x00, 0xf0, 0xf9, 0xf3, 0x00, 0xff, 0xff, 0xff, 0x00, 0xff, 0xfd, 0xff, 0x00, 0x7e, 0xdc, 0xef, + 0x00, 0x26, 0xad, 0xfd, 0x00, 0x2a, 0xaf, 0xf7, 0x00, 0x2d, 0xb2, 0xf2, 0x00, 0x34, 0xb1, 0xe0, 0x00, 0x09, 0xa7, + 0xf7, 0x00, 0x8d, 0xd3, 0xf5, 0x00, 0xfd, 0xfb, 0xf9, 0x00, 0xff, 0xff, 0xf6, 0x00, 0xfd, 0xff, 0xeb, 0x00, 0xfc, + 0xff, 0xe6, 0x00, 0xfc, 0xfc, 0xe0, 0x00, 0xf9, 0xfc, 0xde, 0x00, 0xf7, 0xfc, 0xdd, 0x00, 0xfc, 0xff, 0xef, 0x00, + 0xf9, 0xfd, 0xec, 0x00, 0xe8, 0xf5, 0xd0, 0x00, 0xdf, 0xf5, 0xbd, 0x00, 0xd9, 0xf1, 0xad, 0x00, 0xd2, 0xed, 0x9d, + 0x00, 0xc5, 0xe9, 0x7e, 0x00, 0xb8, 0xe2, 0x6d, 0x00, 0xab, 0xdd, 0x5e, 0x00, 0x9f, 0xd7, 0x4f, 0x00, 0x98, 0xc9, + 0x5f, 0x00, 0x92, 0xc7, 0x35, 0x00, 0x8b, 0xc9, 0x42, 0x00, 0x80, 0xb3, 0x4d, 0x00, 0x00, 0x9b, 0xf2, 0x00, 0x18, + 0x94, 0xf8, 0x00, 0x15, 0x95, 0xf5, 0x00, 0x13, 0x97, 0xf2, 0x00, 0x12, 0x96, 0xf1, 0x00, 0x11, 0x95, 0xf0, 0x00, + 0x11, 0x95, 0xf0, 0x00, 0x56, 0xcf, 0xf4, 0x00, 0x54, 0xcd, 0xf5, 0x00, 0x52, 0xcc, 0xf5, 0x00, 0x51, 0xcb, 0xf7, + 0x00, 0x51, 0xcb, 0xf9, 0x00, 0x49, 0xc8, 0xf1, 0x00, 0x51, 0xd5, 0xfa, 0x00, 0x16, 0x62, 0xc1, 0x00, 0x00, 0x5c, + 0xbb, 0x00, 0x08, 0x74, 0xcd, 0x00, 0x03, 0x7c, 0xce, 0x00, 0x02, 0x8d, 0xd4, 0x00, 0x01, 0x9e, 0xdb, 0x00, 0x09, + 0xae, 0xdc, 0x00, 0x37, 0xc2, 0xee, 0x00, 0x5a, 0xcf, 0xef, 0x00, 0x7e, 0xdc, 0xf0, 0x00, 0x88, 0xe1, 0xf4, 0x00, + 0x92, 0xe6, 0xf8, 0x00, 0xa5, 0xee, 0xf8, 0x00, 0xb9, 0xf5, 0xf9, 0x00, 0xc7, 0xf9, 0xfb, 0x00, 0xd5, 0xfd, 0xfe, + 0x00, 0xdf, 0xfd, 0xfc, 0x00, 0xe9, 0xfd, 0xfa, 0x00, 0xf0, 0xfe, 0xfe, 0x00, 0xf8, 0xff, 0xff, 0x00, 0xfa, 0xff, + 0xfe, 0x00, 0xfd, 0xff, 0xfc, 0x00, 0xfd, 0xfb, 0xff, 0x00, 0x1d, 0xb0, 0xe8, 0x00, 0x2a, 0xb1, 0xee, 0x00, 0x37, + 0xb2, 0xf5, 0x00, 0x25, 0xb9, 0xf7, 0x00, 0x29, 0xb4, 0xf8, 0x00, 0x22, 0xaf, 0xf5, 0x00, 0x1b, 0xaa, 0xf2, 0x00, + 0x9f, 0xd7, 0xf6, 0x00, 0xfd, 0xff, 0xea, 0x00, 0xfc, 0xfe, 0xe0, 0x00, 0xfc, 0xfd, 0xd7, 0x00, 0xf8, 0xfa, 0xda, + 0x00, 0xf4, 0xf7, 0xdd, 0x00, 0xfd, 0xfe, 0xf5, 0x00, 0xf6, 0xfa, 0xe1, 0x00, 0xdf, 0xec, 0xc3, 0x00, 0xd8, 0xef, + 0xb6, 0x00, 0xd2, 0xec, 0xa6, 0x00, 0xcc, 0xea, 0x95, 0x00, 0xbc, 0xe5, 0x67, 0x00, 0xab, 0xdb, 0x56, 0x00, 0x9f, + 0xd3, 0x44, 0x00, 0x92, 0xcb, 0x33, 0x00, 0x85, 0xc8, 0x24, 0x00, 0x79, 0xb4, 0x6a, 0x00, 0x3a, 0x9e, 0xaf, 0x00, + 0x0c, 0x97, 0xff, 0x00, 0x19, 0x94, 0xf9, 0x00, 0x0f, 0x9b, 0xee, 0x00, 0x13, 0x9a, 0xf0, 0x00, 0x16, 0x99, 0xf3, + 0x00, 0x14, 0x97, 0xf1, 0x00, 0x12, 0x95, 0xef, 0x00, 0x12, 0x95, 0xef, 0x00, 0x58, 0xd0, 0xf5, 0x00, 0x56, 0xce, + 0xf5, 0x00, 0x53, 0xcd, 0xf4, 0x00, 0x53, 0xcc, 0xf6, 0x00, 0x52, 0xcb, 0xf8, 0x00, 0x53, 0xd6, 0xfb, 0x00, 0x4f, + 0xc8, 0xfc, 0x00, 0x00, 0x4c, 0xad, 0x00, 0x09, 0x6f, 0xca, 0x00, 0x0b, 0x80, 0xd4, 0x00, 0x05, 0x88, 0xd5, 0x00, + 0x05, 0x98, 0xdb, 0x00, 0x05, 0xa8, 0xe1, 0x00, 0x18, 0xb6, 0xe6, 0x00, 0x3f, 0xc8, 0xf2, 0x00, 0x63, 0xd3, 0xf3, + 0x00, 0x86, 0xdf, 0xf5, 0x00, 0x91, 0xe4, 0xf7, 0x00, 0x9c, 0xe9, 0xfa, 0x00, 0xae, 0xf0, 0xf9, 0x00, 0xc0, 0xf7, + 0xf9, 0x00, 0xcb, 0xfa, 0xfb, 0x00, 0xd7, 0xfd, 0xfd, 0x00, 0xde, 0xfd, 0xfc, 0x00, 0xe6, 0xfe, 0xfb, 0x00, 0xf0, + 0xff, 0xfe, 0x00, 0xfa, 0xff, 0xff, 0x00, 0xf2, 0xfe, 0xfb, 0x00, 0xfe, 0xff, 0xfd, 0x00, 0xc6, 0xe9, 0xfb, 0x00, + 0x1e, 0xb0, 0xec, 0x00, 0x30, 0xb4, 0xf6, 0x00, 0x30, 0xb7, 0xf8, 0x00, 0x19, 0xa8, 0xf7, 0x00, 0x26, 0xb0, 0xf0, + 0x00, 0x22, 0xae, 0xf3, 0x00, 0x1e, 0xab, 0xf5, 0x00, 0x27, 0xaa, 0xfa, 0x00, 0x1c, 0xa6, 0xf6, 0x00, 0x7d, 0xcd, + 0xea, 0x00, 0xdf, 0xf4, 0xdd, 0x00, 0xea, 0xff, 0xb0, 0x00, 0xfd, 0xfe, 0xed, 0x00, 0xff, 0xff, 0xef, 0x00, 0xfc, + 0xf9, 0xd3, 0x00, 0xed, 0xee, 0xb4, 0x00, 0xe6, 0xe9, 0xac, 0x00, 0xd9, 0xe6, 0x8a, 0x00, 0xcb, 0xe3, 0x67, 0x00, + 0xb9, 0xe1, 0x53, 0x00, 0xa6, 0xdd, 0x4d, 0x00, 0x75, 0xc5, 0x7f, 0x00, 0x43, 0xad, 0xb0, 0x00, 0x22, 0x9b, 0xf3, + 0x00, 0x0a, 0x9c, 0xff, 0x00, 0x09, 0x98, 0xf6, 0x00, 0x10, 0x9c, 0xef, 0x00, 0x18, 0x9a, 0xee, 0x00, 0x14, 0x9d, + 0xed, 0x00, 0x15, 0x9b, 0xf0, 0x00, 0x15, 0x99, 0xf2, 0x00, 0x13, 0x97, 0xf0, 0x00, 0x11, 0x95, 0xee, 0x00, 0x11, + 0x95, 0xee, 0x00, 0x5a, 0xd1, 0xf6, 0x00, 0x57, 0xcf, 0xf5, 0x00, 0x54, 0xce, 0xf4, 0x00, 0x54, 0xcd, 0xf6, 0x00, + 0x53, 0xcb, 0xf8, 0x00, 0x4d, 0xd3, 0xf4, 0x00, 0x2c, 0x9a, 0xdd, 0x00, 0x04, 0x5e, 0xc1, 0x00, 0x05, 0x72, 0xc9, + 0x00, 0x06, 0x83, 0xd2, 0x00, 0x07, 0x94, 0xdc, 0x00, 0x08, 0xa2, 0xe2, 0x00, 0x08, 0xb1, 0xe8, 0x00, 0x28, 0xbf, + 0xef, 0x00, 0x48, 0xce, 0xf6, 0x00, 0x6b, 0xd8, 0xf8, 0x00, 0x8f, 0xe3, 0xfa, 0x00, 0x9b, 0xe8, 0xfa, 0x00, 0xa6, + 0xed, 0xfb, 0x00, 0xb7, 0xf3, 0xfb, 0x00, 0xc7, 0xf9, 0xfa, 0x00, 0xd0, 0xfb, 0xfc, 0x00, 0xd9, 0xfd, 0xfd, 0x00, + 0xde, 0xfe, 0xfd, 0x00, 0xe2, 0xff, 0xfc, 0x00, 0xef, 0xff, 0xfe, 0x00, 0xfc, 0xff, 0xff, 0x00, 0xeb, 0xfe, 0xf7, + 0x00, 0xff, 0xff, 0xfe, 0x00, 0x8f, 0xd7, 0xf8, 0x00, 0x1e, 0xb0, 0xf1, 0x00, 0x2e, 0xb0, 0xf6, 0x00, 0x18, 0xab, + 0xec, 0x00, 0xe0, 0xf7, 0xfd, 0x00, 0x24, 0xad, 0xe9, 0x00, 0x23, 0xac, 0xf1, 0x00, 0x21, 0xac, 0xf8, 0x00, 0x26, + 0xae, 0xf7, 0x00, 0x2c, 0xb0, 0xf6, 0x00, 0x1a, 0xa9, 0xf5, 0x00, 0x08, 0xa3, 0xf4, 0x00, 0x22, 0xa7, 0xf9, 0x00, + 0x4c, 0xc2, 0xf2, 0x00, 0x6d, 0xcd, 0xef, 0x00, 0x7e, 0xc9, 0xdb, 0x00, 0x7f, 0xca, 0xc2, 0x00, 0x81, 0xc6, 0xc6, + 0x00, 0x61, 0xbc, 0xcb, 0x00, 0x41, 0xb3, 0xd0, 0x00, 0x24, 0xa7, 0xe9, 0x00, 0x08, 0x9b, 0xff, 0x00, 0x11, 0x9d, + 0xff, 0x00, 0x1a, 0x9f, 0xff, 0x00, 0x0f, 0x99, 0xe9, 0x00, 0x14, 0x9c, 0xf9, 0x00, 0x15, 0x9c, 0xf7, 0x00, 0x15, + 0x9c, 0xf5, 0x00, 0x17, 0x9d, 0xf1, 0x00, 0x19, 0x9e, 0xed, 0x00, 0x17, 0x9c, 0xef, 0x00, 0x15, 0x99, 0xf1, 0x00, + 0x13, 0x97, 0xef, 0x00, 0x11, 0x95, 0xed, 0x00, 0x11, 0x95, 0xed, 0x00, 0x5c, 0xd2, 0xf6, 0x00, 0x59, 0xd0, 0xf5, + 0x00, 0x55, 0xcf, 0xf3, 0x00, 0x54, 0xcd, 0xf5, 0x00, 0x53, 0xcc, 0xf8, 0x00, 0x51, 0xd5, 0xf6, 0x00, 0x16, 0x7b, + 0xcf, 0x00, 0x04, 0x67, 0xc6, 0x00, 0x06, 0x7b, 0xcf, 0x00, 0x06, 0x8b, 0xd7, 0x00, 0x05, 0x9c, 0xdf, 0x00, 0x08, + 0xa9, 0xe5, 0x00, 0x0a, 0xb6, 0xeb, 0x00, 0x2b, 0xc4, 0xf1, 0x00, 0x4c, 0xd2, 0xf7, 0x00, 0x6d, 0xdb, 0xf9, 0x00, + 0x8e, 0xe5, 0xfa, 0x00, 0x9d, 0xea, 0xfb, 0x00, 0xac, 0xef, 0xfb, 0x00, 0xbd, 0xf5, 0xfb, 0x00, 0xce, 0xfb, 0xfa, + 0x00, 0xd5, 0xfb, 0xfc, 0x00, 0xdc, 0xfc, 0xfd, 0x00, 0xdc, 0xfe, 0xfd, 0x00, 0xdd, 0xff, 0xfd, 0x00, 0xe4, 0xff, + 0xfd, 0x00, 0xea, 0xff, 0xfd, 0x00, 0xff, 0xff, 0xfe, 0x00, 0xff, 0xff, 0xff, 0x00, 0x27, 0xc0, 0xde, 0x00, 0x26, + 0xb5, 0xf6, 0x00, 0x1f, 0xb0, 0xf9, 0x00, 0x4d, 0xc6, 0xff, 0x00, 0xff, 0xf9, 0xef, 0x00, 0xfe, 0xff, 0xfa, 0x00, + 0x8b, 0xd8, 0xf7, 0x00, 0x18, 0xa7, 0xf3, 0x00, 0x1d, 0xaa, 0xf4, 0x00, 0x23, 0xac, 0xf6, 0x00, 0x22, 0xac, 0xf3, + 0x00, 0x22, 0xab, 0xf0, 0x00, 0x1a, 0xa3, 0xf2, 0x00, 0x1a, 0xa6, 0xee, 0x00, 0x18, 0xa8, 0xf5, 0x00, 0x0e, 0xa2, + 0xf3, 0x00, 0x11, 0xa4, 0xf2, 0x00, 0x14, 0xa4, 0xff, 0x00, 0x15, 0xa3, 0xfc, 0x00, 0x16, 0xa3, 0xfa, 0x00, 0x17, + 0xa2, 0xf3, 0x00, 0x19, 0xa2, 0xec, 0x00, 0x0e, 0x99, 0xfe, 0x00, 0x16, 0x9b, 0xed, 0x00, 0x00, 0xa1, 0xff, 0x00, + 0x2b, 0x9d, 0xe8, 0x00, 0x61, 0xb5, 0xb0, 0x00, 0x10, 0x9a, 0xf7, 0x00, 0x14, 0x9c, 0xf2, 0x00, 0x18, 0x9e, 0xed, + 0x00, 0x16, 0x9c, 0xef, 0x00, 0x14, 0x9a, 0xf0, 0x00, 0x12, 0x98, 0xee, 0x00, 0x10, 0x96, 0xec, 0x00, 0x10, 0x96, + 0xec, 0x00, 0x5f, 0xd3, 0xf7, 0x00, 0x5b, 0xd2, 0xf5, 0x00, 0x56, 0xd0, 0xf3, 0x00, 0x55, 0xce, 0xf5, 0x00, 0x53, + 0xcd, 0xf7, 0x00, 0x56, 0xd8, 0xf8, 0x00, 0x00, 0x5c, 0xc0, 0x00, 0x03, 0x70, 0xcb, 0x00, 0x07, 0x85, 0xd6, 0x00, + 0x05, 0x94, 0xdc, 0x00, 0x04, 0xa3, 0xe2, 0x00, 0x08, 0xaf, 0xe8, 0x00, 0x0c, 0xbc, 0xee, 0x00, 0x2e, 0xc8, 0xf3, + 0x00, 0x50, 0xd5, 0xf9, 0x00, 0x6f, 0xde, 0xfa, 0x00, 0x8d, 0xe7, 0xfb, 0x00, 0x9f, 0xec, 0xfb, 0x00, 0xb1, 0xf2, + 0xfb, 0x00, 0xc3, 0xf7, 0xfb, 0x00, 0xd4, 0xfc, 0xfa, 0x00, 0xd9, 0xfc, 0xfc, 0x00, 0xde, 0xfc, 0xfd, 0x00, 0xdb, + 0xfd, 0xfd, 0x00, 0xd9, 0xff, 0xfd, 0x00, 0xd9, 0xfd, 0xfb, 0x00, 0xd9, 0xfc, 0xfa, 0x00, 0xe5, 0xfa, 0xfa, 0x00, + 0xa4, 0xea, 0xf7, 0x00, 0x2b, 0xad, 0xfb, 0x00, 0x2f, 0xb9, 0xfa, 0x00, 0x1a, 0xae, 0xed, 0x00, 0x99, 0xdb, 0xf8, + 0x00, 0xff, 0xff, 0xff, 0x00, 0xfe, 0xfd, 0xfc, 0x00, 0xff, 0xfe, 0xfd, 0x00, 0xff, 0xff, 0xfd, 0x00, 0x8c, 0xd4, + 0xfa, 0x00, 0x19, 0xa9, 0xf6, 0x00, 0x18, 0xa9, 0xf7, 0x00, 0x16, 0xaa, 0xf9, 0x00, 0x1a, 0xa7, 0xf3, 0x00, 0x1e, + 0xa5, 0xee, 0x00, 0x1f, 0xa7, 0xf2, 0x00, 0x21, 0xa9, 0xf6, 0x00, 0x1e, 0xa7, 0xf7, 0x00, 0x1b, 0xa5, 0xf7, 0x00, + 0x17, 0xa4, 0xf9, 0x00, 0x12, 0xa2, 0xfb, 0x00, 0x0b, 0x9d, 0xfd, 0x00, 0x03, 0x99, 0xfe, 0x00, 0x26, 0xa2, 0xfa, + 0x00, 0x6f, 0xc0, 0xb0, 0x00, 0xcf, 0xca, 0x5e, 0x00, 0xff, 0xe5, 0x28, 0x00, 0x74, 0xb4, 0xb3, 0x00, 0x0b, 0x98, + 0xfa, 0x00, 0x11, 0x9a, 0xf4, 0x00, 0x17, 0x9d, 0xee, 0x00, 0x15, 0x9c, 0xee, 0x00, 0x13, 0x9a, 0xef, 0x00, 0x11, + 0x98, 0xed, 0x00, 0x0f, 0x96, 0xeb, 0x00, 0x0f, 0x96, 0xeb, 0x00, 0x5d, 0xd1, 0xf6, 0x00, 0x5b, 0xd2, 0xf5, 0x00, + 0x58, 0xd2, 0xf4, 0x00, 0x53, 0xce, 0xf4, 0x00, 0x56, 0xd2, 0xfb, 0x00, 0x40, 0xb2, 0xe6, 0x00, 0x01, 0x64, 0xc6, + 0x00, 0x03, 0x76, 0xcf, 0x00, 0x04, 0x87, 0xd7, 0x00, 0x02, 0x96, 0xdd, 0x00, 0x01, 0xa4, 0xe4, 0x00, 0x04, 0xb1, + 0xea, 0x00, 0x07, 0xbd, 0xf1, 0x00, 0x1b, 0xc8, 0xf2, 0x00, 0x43, 0xd5, 0xfc, 0x00, 0x64, 0xdd, 0xfb, 0x00, 0x85, + 0xe6, 0xfb, 0x00, 0x98, 0xeb, 0xfc, 0x00, 0xac, 0xf1, 0xfd, 0x00, 0xbe, 0xf9, 0xff, 0x00, 0xcf, 0xff, 0xff, 0x00, + 0xcf, 0xfd, 0xff, 0x00, 0xcf, 0xf9, 0xfb, 0x00, 0xd2, 0xfe, 0xfe, 0x00, 0xd5, 0xff, 0xff, 0x00, 0xc6, 0xf9, 0xff, + 0x00, 0xb8, 0xef, 0xff, 0x00, 0x5a, 0xd7, 0xd9, 0x00, 0x40, 0xb9, 0xe9, 0x00, 0x2f, 0xb9, 0xff, 0x00, 0x2b, 0xb2, + 0xf0, 0x00, 0x28, 0xaf, 0xeb, 0x00, 0xde, 0xf0, 0xf2, 0x00, 0xff, 0xff, 0xff, 0x00, 0xfe, 0xff, 0xff, 0x00, 0xff, + 0xfe, 0xfe, 0x00, 0xff, 0xfe, 0xfa, 0x00, 0xff, 0xff, 0xfa, 0x00, 0xff, 0xff, 0xf9, 0x00, 0xc2, 0xe8, 0xf0, 0x00, + 0x84, 0xcd, 0xe7, 0x00, 0x53, 0xbb, 0xe9, 0x00, 0x22, 0xa9, 0xeb, 0x00, 0x14, 0xa1, 0xff, 0x00, 0x06, 0x9f, 0xf8, + 0x00, 0x0f, 0xa0, 0xf8, 0x00, 0x19, 0xa3, 0xeb, 0x00, 0x43, 0xb1, 0xe1, 0x00, 0x6e, 0xc2, 0xc9, 0x00, 0xb0, 0xd7, + 0x9a, 0x00, 0xf2, 0xeb, 0x6b, 0x00, 0xeb, 0xee, 0x32, 0x00, 0xf8, 0xe6, 0x47, 0x00, 0xff, 0xe2, 0x3a, 0x00, 0xfd, + 0xe1, 0x42, 0x00, 0x00, 0x98, 0xf4, 0x00, 0x19, 0xa1, 0xfc, 0x00, 0x16, 0x9e, 0xf7, 0x00, 0x12, 0x9b, 0xf1, 0x00, + 0x13, 0x9a, 0xf1, 0x00, 0x14, 0x9a, 0xf0, 0x00, 0x12, 0x98, 0xee, 0x00, 0x10, 0x96, 0xec, 0x00, 0x10, 0x96, 0xec, + 0x00, 0x5c, 0xcf, 0xf6, 0x00, 0x5b, 0xd2, 0xf6, 0x00, 0x5a, 0xd4, 0xf6, 0x00, 0x52, 0xcd, 0xf2, 0x00, 0x5a, 0xd6, + 0xfe, 0x00, 0x29, 0x8c, 0xd5, 0x00, 0x02, 0x6c, 0xcc, 0x00, 0x02, 0x7b, 0xd2, 0x00, 0x01, 0x89, 0xd8, 0x00, 0x00, + 0x97, 0xdf, 0x00, 0x00, 0xa6, 0xe6, 0x00, 0x00, 0xb2, 0xed, 0x00, 0x02, 0xbe, 0xf4, 0x00, 0x09, 0xc7, 0xf1, 0x00, + 0x35, 0xd5, 0xff, 0x00, 0x59, 0xdd, 0xfd, 0x00, 0x7c, 0xe5, 0xfb, 0x00, 0x91, 0xea, 0xfd, 0x00, 0xa6, 0xf0, 0xff, + 0x00, 0xb1, 0xf2, 0xff, 0x00, 0xbb, 0xf5, 0xff, 0x00, 0xbe, 0xf5, 0xfc, 0x00, 0xc1, 0xf6, 0xf9, 0x00, 0xc1, 0xf7, + 0xf7, 0x00, 0xc1, 0xf9, 0xf4, 0x00, 0xc7, 0xfd, 0xfc, 0x00, 0xcd, 0xff, 0xff, 0x00, 0xc2, 0xf9, 0xf8, 0x00, 0x5a, + 0xcd, 0xf4, 0x00, 0x39, 0xb1, 0xf3, 0x00, 0x38, 0xba, 0xf5, 0x00, 0x2a, 0xb4, 0xf7, 0x00, 0xfc, 0xfb, 0xf8, 0x00, + 0xfd, 0xfe, 0xff, 0x00, 0xfe, 0xff, 0xff, 0x00, 0xff, 0xfe, 0xff, 0x00, 0xff, 0xfc, 0xf6, 0x00, 0xfd, 0xfe, 0xf2, + 0x00, 0xf7, 0xff, 0xee, 0x00, 0xfc, 0xff, 0xea, 0x00, 0xff, 0xff, 0xe5, 0x00, 0xff, 0xff, 0xd8, 0x00, 0xff, 0xff, + 0xcb, 0x00, 0xff, 0xfb, 0xf1, 0x00, 0xff, 0xff, 0xdf, 0x00, 0xfd, 0xfd, 0xc2, 0x00, 0xf7, 0xff, 0x88, 0x00, 0xfb, + 0xfe, 0x92, 0x00, 0xff, 0xff, 0x7f, 0x00, 0xfd, 0xfc, 0x6c, 0x00, 0xfa, 0xf7, 0x59, 0x00, 0xf8, 0xf0, 0x59, 0x00, + 0xf7, 0xe9, 0x58, 0x00, 0xf7, 0xe3, 0x59, 0x00, 0xd0, 0xd3, 0x68, 0x00, 0x09, 0x98, 0xff, 0x00, 0x18, 0x9a, 0xef, + 0x00, 0x12, 0x9a, 0xf2, 0x00, 0x0c, 0x99, 0xf5, 0x00, 0x11, 0x99, 0xf3, 0x00, 0x15, 0x99, 0xf2, 0x00, 0x13, 0x97, + 0xf0, 0x00, 0x11, 0x95, 0xee, 0x00, 0x11, 0x95, 0xee, 0x00, 0x5f, 0xd2, 0xf9, 0x00, 0x5c, 0xd3, 0xf8, 0x00, 0x59, + 0xd4, 0xf6, 0x00, 0x58, 0xd3, 0xf8, 0x00, 0x5e, 0xda, 0xff, 0x00, 0x19, 0x71, 0xcd, 0x00, 0x02, 0x6e, 0xcd, 0x00, + 0x03, 0x7b, 0xd3, 0x00, 0x04, 0x88, 0xd9, 0x00, 0x04, 0x97, 0xe0, 0x00, 0x05, 0xa6, 0xe6, 0x00, 0x01, 0xad, 0xe7, + 0x00, 0x00, 0xb5, 0xe8, 0x00, 0x07, 0xbe, 0xea, 0x00, 0x23, 0xcb, 0xf5, 0x00, 0x4c, 0xd7, 0xf8, 0x00, 0x74, 0xe4, + 0xfc, 0x00, 0x89, 0xe8, 0xfd, 0x00, 0x9f, 0xec, 0xfe, 0x00, 0xa5, 0xed, 0xfe, 0x00, 0xab, 0xef, 0xfe, 0x00, 0xae, + 0xef, 0xfc, 0x00, 0xb0, 0xef, 0xf9, 0x00, 0xb3, 0xf3, 0xf9, 0x00, 0xb6, 0xf6, 0xf8, 0x00, 0xb6, 0xf9, 0xfc, 0x00, + 0xb5, 0xfc, 0xff, 0x00, 0xda, 0xf3, 0xff, 0x00, 0x1a, 0xb9, 0xf1, 0x00, 0x28, 0xb3, 0xf4, 0x00, 0x2b, 0xb3, 0xf6, + 0x00, 0x73, 0xce, 0xf4, 0x00, 0xfd, 0xfd, 0xf5, 0x00, 0xfd, 0xfe, 0xfa, 0x00, 0xfd, 0xff, 0xfe, 0x00, 0xff, 0xfe, + 0xf9, 0x00, 0xff, 0xfd, 0xf3, 0x00, 0xfd, 0xfe, 0xee, 0x00, 0xfa, 0xff, 0xe9, 0x00, 0xfd, 0xff, 0xe4, 0x00, 0xff, + 0xff, 0xde, 0x00, 0xff, 0xff, 0xd0, 0x00, 0xff, 0xff, 0xc2, 0x00, 0xfd, 0xfa, 0xd7, 0x00, 0xff, 0xfc, 0xf3, 0x00, + 0xff, 0xff, 0xc0, 0x00, 0xfc, 0xfb, 0xc5, 0x00, 0xfc, 0xff, 0x84, 0x00, 0xfc, 0xfb, 0x8b, 0x00, 0xfb, 0xf6, 0x7a, + 0x00, 0xf9, 0xf2, 0x69, 0x00, 0xf7, 0xed, 0x5e, 0x00, 0xf4, 0xe9, 0x54, 0x00, 0xf7, 0xe9, 0x48, 0x00, 0x87, 0xbd, + 0xa9, 0x00, 0x10, 0x9a, 0xfc, 0x00, 0x17, 0x9c, 0xf2, 0x00, 0x14, 0x9b, 0xf1, 0x00, 0x11, 0x9a, 0xf1, 0x00, 0x13, + 0x99, 0xf2, 0x00, 0x16, 0x98, 0xf3, 0x00, 0x14, 0x96, 0xf1, 0x00, 0x12, 0x94, 0xef, 0x00, 0x12, 0x94, 0xef, 0x00, + 0x62, 0xd4, 0xfc, 0x00, 0x5d, 0xd4, 0xf9, 0x00, 0x59, 0xd4, 0xf6, 0x00, 0x56, 0xd1, 0xf6, 0x00, 0x53, 0xce, 0xf5, + 0x00, 0x01, 0x4e, 0xbe, 0x00, 0x02, 0x6f, 0xcd, 0x00, 0x05, 0x7b, 0xd4, 0x00, 0x07, 0x87, 0xda, 0x00, 0x09, 0x96, + 0xe0, 0x00, 0x0c, 0xa5, 0xe7, 0x00, 0x0b, 0xb0, 0xe9, 0x00, 0x09, 0xbb, 0xeb, 0x00, 0x15, 0xc5, 0xf3, 0x00, 0x21, + 0xd0, 0xfc, 0x00, 0x46, 0xda, 0xfc, 0x00, 0x6c, 0xe3, 0xfc, 0x00, 0x82, 0xe6, 0xfd, 0x00, 0x97, 0xe9, 0xfe, 0x00, + 0x99, 0xe9, 0xfe, 0x00, 0x9c, 0xe8, 0xfe, 0x00, 0x9e, 0xe9, 0xfb, 0x00, 0xa0, 0xe9, 0xf9, 0x00, 0xa6, 0xee, 0xfa, + 0x00, 0xac, 0xf3, 0xfc, 0x00, 0xb0, 0xef, 0xfc, 0x00, 0xb5, 0xec, 0xfb, 0x00, 0x89, 0xdd, 0xf9, 0x00, 0x28, 0xb4, + 0xf3, 0x00, 0x3e, 0xbe, 0xf7, 0x00, 0x1e, 0xad, 0xf7, 0x00, 0xbd, 0xe8, 0xf0, 0x00, 0xfe, 0xff, 0xf2, 0x00, 0xfe, + 0xff, 0xf3, 0x00, 0xfd, 0xff, 0xf4, 0x00, 0xfe, 0xfe, 0xf2, 0x00, 0xfe, 0xfe, 0xf0, 0x00, 0xfe, 0xfe, 0xea, 0x00, + 0xfe, 0xfe, 0xe4, 0x00, 0xfe, 0xfe, 0xde, 0x00, 0xfe, 0xfe, 0xd8, 0x00, 0xfc, 0xff, 0xc9, 0x00, 0xfb, 0xff, 0xba, + 0x00, 0xf6, 0xfe, 0xa0, 0x00, 0xff, 0xff, 0xce, 0x00, 0xff, 0xf9, 0xf6, 0x00, 0xff, 0xff, 0xc9, 0x00, 0xfd, 0xf7, + 0xbe, 0x00, 0xf8, 0xf8, 0x7a, 0x00, 0xf9, 0xf6, 0x6b, 0x00, 0xf9, 0xf3, 0x5c, 0x00, 0xf5, 0xee, 0x56, 0x00, 0xf1, + 0xe8, 0x4f, 0x00, 0xf8, 0xee, 0x37, 0x00, 0x3f, 0xa7, 0xea, 0x00, 0x18, 0x9d, 0xf5, 0x00, 0x17, 0x9d, 0xf4, 0x00, + 0x16, 0x9c, 0xf1, 0x00, 0x15, 0x9b, 0xee, 0x00, 0x16, 0x9a, 0xf2, 0x00, 0x17, 0x98, 0xf5, 0x00, 0x15, 0x96, 0xf3, + 0x00, 0x13, 0x94, 0xf1, 0x00, 0x13, 0x94, 0xf1, 0x00, 0x66, 0xd7, 0xfc, 0x00, 0x5f, 0xd1, 0xf5, 0x00, 0x60, 0xd4, + 0xf6, 0x00, 0x59, 0xd8, 0xf9, 0x00, 0x39, 0x9d, 0xdb, 0x00, 0x08, 0x58, 0xbe, 0x00, 0x09, 0x6c, 0xcd, 0x00, 0x0c, + 0x7a, 0xd2, 0x00, 0x10, 0x87, 0xd7, 0x00, 0x12, 0x96, 0xdf, 0x00, 0x13, 0xa6, 0xe8, 0x00, 0x13, 0xb0, 0xeb, 0x00, + 0x1b, 0xc3, 0xf5, 0x00, 0x0f, 0xc8, 0xf3, 0x00, 0x17, 0xd0, 0xf9, 0x00, 0x27, 0xd3, 0xf4, 0x00, 0x4b, 0xd7, 0xf7, + 0x00, 0x61, 0xdb, 0xf8, 0x00, 0x77, 0xde, 0xf9, 0x00, 0x7f, 0xe0, 0xfa, 0x00, 0x88, 0xe1, 0xfa, 0x00, 0x8d, 0xe4, + 0xfb, 0x00, 0x91, 0xe7, 0xfb, 0x00, 0x96, 0xea, 0xfc, 0x00, 0x9a, 0xed, 0xfd, 0x00, 0x9f, 0xea, 0xfb, 0x00, 0xa3, + 0xe7, 0xfa, 0x00, 0x5e, 0xcc, 0xfb, 0x00, 0x2d, 0xb7, 0xf5, 0x00, 0x24, 0xb8, 0xf9, 0x00, 0x14, 0xb1, 0xf5, 0x00, + 0xff, 0xfb, 0xff, 0x00, 0xfe, 0xff, 0xec, 0x00, 0xff, 0xff, 0xed, 0x00, 0xff, 0xff, 0xee, 0x00, 0xff, 0xff, 0xec, + 0x00, 0xfe, 0xfd, 0xeb, 0x00, 0xfe, 0xfd, 0xe4, 0x00, 0xfe, 0xfd, 0xdd, 0x00, 0xfe, 0xfe, 0xd6, 0x00, 0xfe, 0xfe, + 0xce, 0x00, 0xfc, 0xfd, 0xc1, 0x00, 0xfc, 0xfc, 0xb5, 0x00, 0xf6, 0xfb, 0x8d, 0x00, 0xf8, 0xfc, 0x8a, 0x00, 0xf8, + 0xfa, 0xcc, 0x00, 0xf8, 0xfe, 0xf2, 0x00, 0xf9, 0xff, 0xbe, 0x00, 0xfb, 0xf9, 0xc2, 0x00, 0xfb, 0xf8, 0xac, 0x00, + 0xfc, 0xf7, 0x96, 0x00, 0xfa, 0xf4, 0x91, 0x00, 0xf7, 0xf1, 0x8d, 0x00, 0xff, 0xe5, 0xa9, 0x00, 0x00, 0x96, 0xf7, + 0x00, 0x08, 0x9a, 0xf7, 0x00, 0x15, 0x9e, 0xf7, 0x00, 0x16, 0x9d, 0xf4, 0x00, 0x16, 0x9c, 0xf0, 0x00, 0x16, 0x9b, + 0xf2, 0x00, 0x16, 0x99, 0xf4, 0x00, 0x14, 0x97, 0xf3, 0x00, 0x13, 0x96, 0xf1, 0x00, 0x13, 0x96, 0xf1, 0x00, 0x6b, + 0xd9, 0xfb, 0x00, 0x61, 0xce, 0xf1, 0x00, 0x67, 0xd3, 0xf7, 0x00, 0x5c, 0xde, 0xfd, 0x00, 0x1f, 0x6c, 0xc0, 0x00, + 0x0f, 0x63, 0xbf, 0x00, 0x0f, 0x6a, 0xcd, 0x00, 0x14, 0x78, 0xd1, 0x00, 0x18, 0x87, 0xd4, 0x00, 0x19, 0x97, 0xdf, + 0x00, 0x1a, 0xa6, 0xe9, 0x00, 0x14, 0xa9, 0xe4, 0x00, 0x1d, 0xbb, 0xef, 0x00, 0x0d, 0xbe, 0xeb, 0x00, 0x23, 0xc5, + 0xf6, 0x00, 0x13, 0xc6, 0xed, 0x00, 0x2a, 0xcb, 0xf3, 0x00, 0x40, 0xcf, 0xf4, 0x00, 0x56, 0xd4, 0xf4, 0x00, 0x65, + 0xd7, 0xf6, 0x00, 0x74, 0xda, 0xf7, 0x00, 0x7b, 0xdf, 0xfb, 0x00, 0x83, 0xe5, 0xfe, 0x00, 0x86, 0xe6, 0xfe, 0x00, + 0x89, 0xe8, 0xfd, 0x00, 0x8e, 0xe5, 0xfb, 0x00, 0x92, 0xe2, 0xfa, 0x00, 0x33, 0xbc, 0xfc, 0x00, 0x32, 0xb9, 0xf7, + 0x00, 0x31, 0xba, 0xfd, 0x00, 0x57, 0xc5, 0xf7, 0x00, 0xf4, 0xff, 0xde, 0x00, 0xfd, 0xff, 0xe7, 0x00, 0xff, 0xff, + 0xe7, 0x00, 0xff, 0xff, 0xe7, 0x00, 0xff, 0xff, 0xe6, 0x00, 0xfd, 0xfc, 0xe6, 0x00, 0xfd, 0xfd, 0xdd, 0x00, 0xfd, + 0xfd, 0xd5, 0x00, 0xfd, 0xfd, 0xcd, 0x00, 0xfe, 0xfd, 0xc5, 0x00, 0xfd, 0xfa, 0xba, 0x00, 0xfc, 0xf8, 0xaf, 0x00, + 0xfe, 0xf9, 0x9f, 0x00, 0xff, 0xfb, 0x8e, 0x00, 0xfa, 0xfe, 0x77, 0x00, 0xf4, 0xfb, 0x7d, 0x00, 0xf9, 0xf8, 0xd2, + 0x00, 0xfd, 0xff, 0xee, 0x00, 0xfe, 0xfe, 0xdf, 0x00, 0xff, 0xfc, 0xd0, 0x00, 0xfe, 0xfa, 0xcd, 0x00, 0xfd, 0xf9, + 0xca, 0x00, 0xa6, 0xd3, 0xce, 0x00, 0x03, 0x99, 0xeb, 0x00, 0x1e, 0xa1, 0xec, 0x00, 0x14, 0x9f, 0xfa, 0x00, 0x15, + 0x9e, 0xf6, 0x00, 0x17, 0x9e, 0xf2, 0x00, 0x16, 0x9c, 0xf3, 0x00, 0x15, 0x9a, 0xf3, 0x00, 0x14, 0x99, 0xf2, 0x00, + 0x13, 0x98, 0xf1, 0x00, 0x13, 0x98, 0xf1, 0x00, 0x55, 0xd4, 0xf4, 0x00, 0x5b, 0xd1, 0xf1, 0x00, 0x69, 0xd6, 0xf6, + 0x00, 0x6e, 0xe2, 0xff, 0x00, 0x0c, 0x50, 0xa8, 0x00, 0x11, 0x61, 0xbe, 0x00, 0x0f, 0x6a, 0xcd, 0x00, 0x1f, 0x83, + 0xd6, 0x00, 0x1f, 0x89, 0xdc, 0x00, 0x0f, 0x8c, 0xdd, 0x00, 0x1a, 0x9b, 0xe0, 0x00, 0x22, 0xb1, 0xf4, 0x00, 0x1d, + 0xab, 0xe1, 0x00, 0x14, 0xae, 0xdf, 0x00, 0x26, 0xbd, 0xee, 0x00, 0x15, 0xba, 0xe7, 0x00, 0x1f, 0xc1, 0xef, 0x00, + 0x25, 0xc7, 0xef, 0x00, 0x2b, 0xcd, 0xef, 0x00, 0x3d, 0xcd, 0xf1, 0x00, 0x4e, 0xce, 0xf3, 0x00, 0x5b, 0xd6, 0xf9, + 0x00, 0x68, 0xde, 0xfe, 0x00, 0x6e, 0xdd, 0xfc, 0x00, 0x73, 0xdd, 0xfb, 0x00, 0x76, 0xdd, 0xf5, 0x00, 0x70, 0xd3, + 0xf7, 0x00, 0x31, 0xba, 0xfb, 0x00, 0x33, 0xb9, 0xf6, 0x00, 0x24, 0xb6, 0xff, 0x00, 0xa4, 0xde, 0xe5, 0x00, 0xf9, + 0xff, 0xdc, 0x00, 0xfd, 0xfe, 0xdc, 0x00, 0xff, 0xff, 0xdc, 0x00, 0xff, 0xff, 0xdc, 0x00, 0xfe, 0xfe, 0xdb, 0x00, + 0xfc, 0xfd, 0xda, 0x00, 0xfd, 0xfd, 0xd2, 0x00, 0xfd, 0xfd, 0xcb, 0x00, 0xfd, 0xfd, 0xc3, 0x00, 0xfe, 0xfd, 0xbc, + 0x00, 0xfd, 0xfb, 0xaf, 0x00, 0xfc, 0xfa, 0xa2, 0x00, 0xfd, 0xfb, 0x93, 0x00, 0xfe, 0xfb, 0x83, 0x00, 0xfc, 0xfd, + 0x6b, 0x00, 0xf9, 0xfc, 0x60, 0x00, 0xfb, 0xf8, 0x5d, 0x00, 0xfd, 0xf7, 0x4c, 0x00, 0xfe, 0xf5, 0x76, 0x00, 0xff, + 0xf2, 0xa1, 0x00, 0xf6, 0xec, 0x87, 0x00, 0xf8, 0xe3, 0x60, 0x00, 0x51, 0xbb, 0xb4, 0x00, 0x0d, 0x9a, 0xfe, 0x00, + 0x1a, 0x9e, 0xf7, 0x00, 0x15, 0x9e, 0xf6, 0x00, 0x15, 0x9d, 0xf4, 0x00, 0x15, 0x9d, 0xf2, 0x00, 0x14, 0x9b, 0xf2, + 0x00, 0x12, 0x99, 0xf2, 0x00, 0x12, 0x99, 0xf2, 0x00, 0x12, 0x99, 0xf2, 0x00, 0x12, 0x99, 0xf2, 0x00, 0x67, 0xd4, + 0xfd, 0x00, 0x69, 0xd6, 0xf9, 0x00, 0x6c, 0xd9, 0xf5, 0x00, 0x4f, 0xb7, 0xdc, 0x00, 0x19, 0x53, 0xaf, 0x00, 0x1c, + 0x67, 0xc6, 0x00, 0x00, 0x5a, 0xbd, 0x00, 0x1a, 0x7e, 0xca, 0x00, 0x15, 0x7b, 0xd4, 0x00, 0x05, 0x81, 0xdc, 0x00, + 0x2a, 0xa1, 0xe7, 0x00, 0x01, 0x89, 0xd3, 0x00, 0x2d, 0xab, 0xe3, 0x00, 0x23, 0xa7, 0xdc, 0x00, 0x29, 0xb4, 0xe6, + 0x00, 0x17, 0xad, 0xe1, 0x00, 0x14, 0xb7, 0xec, 0x00, 0x15, 0xb9, 0xea, 0x00, 0x16, 0xbb, 0xe9, 0x00, 0x1f, 0xbf, + 0xec, 0x00, 0x28, 0xc2, 0xef, 0x00, 0x3b, 0xcd, 0xf7, 0x00, 0x4e, 0xd8, 0xff, 0x00, 0x56, 0xd5, 0xfb, 0x00, 0x5d, + 0xd2, 0xf8, 0x00, 0x5e, 0xd6, 0xf0, 0x00, 0x4e, 0xc5, 0xf4, 0x00, 0x2f, 0xb9, 0xfa, 0x00, 0x35, 0xb8, 0xf4, 0x00, + 0x17, 0xb1, 0xff, 0x00, 0xf0, 0xf7, 0xd2, 0x00, 0xfe, 0xff, 0xda, 0x00, 0xfd, 0xfc, 0xd2, 0x00, 0xfd, 0xfd, 0xd1, + 0x00, 0xfd, 0xfe, 0xd1, 0x00, 0xfd, 0xfe, 0xcf, 0x00, 0xfc, 0xfe, 0xcd, 0x00, 0xfc, 0xfd, 0xc7, 0x00, 0xfd, 0xfd, + 0xc0, 0x00, 0xfd, 0xfd, 0xb9, 0x00, 0xfd, 0xfd, 0xb2, 0x00, 0xfd, 0xfc, 0xa4, 0x00, 0xfd, 0xfc, 0x95, 0x00, 0xfd, + 0xfc, 0x87, 0x00, 0xfd, 0xfc, 0x79, 0x00, 0xfd, 0xfa, 0x6c, 0x00, 0xfe, 0xf8, 0x5f, 0x00, 0xf9, 0xf6, 0x45, 0x00, + 0xf6, 0xef, 0x47, 0x00, 0xf2, 0xe9, 0x38, 0x00, 0xef, 0xe4, 0x28, 0x00, 0xee, 0xe4, 0x25, 0x00, 0xff, 0xdd, 0x05, + 0x00, 0x03, 0x99, 0xff, 0x00, 0x17, 0xa1, 0xf5, 0x00, 0x17, 0x9e, 0xf4, 0x00, 0x16, 0x9c, 0xf3, 0x00, 0x15, 0x9c, + 0xf3, 0x00, 0x14, 0x9c, 0xf3, 0x00, 0x12, 0x9b, 0xf1, 0x00, 0x10, 0x99, 0xf0, 0x00, 0x11, 0x9a, 0xf1, 0x00, 0x12, + 0x9b, 0xf2, 0x00, 0x12, 0x9b, 0xf2, 0x00, 0x66, 0xd5, 0xfb, 0x00, 0x70, 0xd5, 0xfc, 0x00, 0x78, 0xe2, 0xff, 0x00, + 0x3b, 0x86, 0xc7, 0x00, 0x23, 0x5f, 0xba, 0x00, 0x1e, 0x6a, 0xba, 0x00, 0x22, 0x7a, 0xd1, 0x00, 0x27, 0x87, 0xd8, + 0x00, 0x24, 0x8c, 0xd7, 0x00, 0x1d, 0x8d, 0xd4, 0x00, 0x21, 0x89, 0xd1, 0x00, 0x2c, 0xa1, 0xea, 0x00, 0x22, 0x96, + 0xd5, 0x00, 0x31, 0xaa, 0xef, 0x00, 0x20, 0xa1, 0xdb, 0x00, 0x17, 0xa1, 0xdd, 0x00, 0x0e, 0xa1, 0xe0, 0x00, 0x1a, + 0xac, 0xe3, 0x00, 0x13, 0xb1, 0xeb, 0x00, 0x10, 0xb8, 0xed, 0x00, 0x0d, 0xc0, 0xef, 0x00, 0x1c, 0xc1, 0xef, 0x00, + 0x2c, 0xc3, 0xf0, 0x00, 0x36, 0xc4, 0xf2, 0x00, 0x40, 0xc5, 0xf4, 0x00, 0x47, 0xc9, 0xf2, 0x00, 0x45, 0xc3, 0xf6, + 0x00, 0x31, 0xba, 0xfa, 0x00, 0x31, 0xb7, 0xf7, 0x00, 0x4c, 0xc2, 0xf4, 0x00, 0xf5, 0xfa, 0xc0, 0x00, 0xfd, 0xff, + 0xc6, 0x00, 0xfd, 0xfc, 0xc5, 0x00, 0xfd, 0xfd, 0xc4, 0x00, 0xfd, 0xfd, 0xc4, 0x00, 0xfc, 0xfd, 0xc2, 0x00, 0xfb, + 0xfd, 0xc1, 0x00, 0xf8, 0xf9, 0xb6, 0x00, 0xfd, 0xfd, 0xb3, 0x00, 0xfd, 0xfd, 0xab, 0x00, 0xfd, 0xfc, 0xa3, 0x00, + 0xfc, 0xfc, 0x95, 0x00, 0xfc, 0xfb, 0x88, 0x00, 0xfc, 0xfb, 0x7b, 0x00, 0xfb, 0xfb, 0x6d, 0x00, 0xfc, 0xf9, 0x62, + 0x00, 0xfc, 0xf7, 0x57, 0x00, 0xf8, 0xf2, 0x45, 0x00, 0xf4, 0xeb, 0x41, 0x00, 0xf0, 0xe5, 0x32, 0x00, 0xeb, 0xe0, + 0x23, 0x00, 0xfb, 0xe0, 0x1c, 0x00, 0xc5, 0xd2, 0x44, 0x00, 0x0a, 0xa2, 0xfe, 0x00, 0x16, 0x9f, 0xf9, 0x00, 0x17, + 0x9f, 0xf6, 0x00, 0x18, 0x9f, 0xf3, 0x00, 0x17, 0x9e, 0xf2, 0x00, 0x15, 0x9d, 0xf2, 0x00, 0x17, 0x9f, 0xf5, 0x00, + 0x18, 0xa1, 0xf8, 0x00, 0x15, 0x9e, 0xf5, 0x00, 0x12, 0x9b, 0xf2, 0x00, 0x12, 0x9b, 0xf2, 0x00, 0x65, 0xd7, 0xfa, + 0x00, 0x64, 0xd1, 0xf7, 0x00, 0x5d, 0xe7, 0xff, 0x00, 0x04, 0x43, 0x9b, 0x00, 0x0e, 0x4c, 0xa5, 0x00, 0x31, 0x7b, + 0xcd, 0x00, 0x04, 0x55, 0xc1, 0x00, 0x00, 0x53, 0xc9, 0x00, 0x03, 0x68, 0xc6, 0x00, 0x26, 0x87, 0xca, 0x00, 0x28, + 0x81, 0xca, 0x00, 0x27, 0x89, 0xd1, 0x00, 0x27, 0x91, 0xd7, 0x00, 0x07, 0x74, 0xc9, 0x00, 0x17, 0x8d, 0xcf, 0x00, + 0x1f, 0x9c, 0xe1, 0x00, 0x17, 0x9b, 0xe4, 0x00, 0x1e, 0x9e, 0xda, 0x00, 0x00, 0x97, 0xde, 0x00, 0x03, 0xa5, 0xe6, + 0x00, 0x08, 0xb1, 0xee, 0x00, 0x09, 0xb0, 0xe8, 0x00, 0x0a, 0xaf, 0xe2, 0x00, 0x17, 0xb4, 0xe9, 0x00, 0x24, 0xb9, + 0xef, 0x00, 0x30, 0xbd, 0xf4, 0x00, 0x3c, 0xc1, 0xf9, 0x00, 0x34, 0xbc, 0xf9, 0x00, 0x2c, 0xb6, 0xf9, 0x00, 0x80, + 0xd2, 0xe8, 0x00, 0xfa, 0xfd, 0xaf, 0x00, 0xfc, 0xfd, 0xb3, 0x00, 0xfd, 0xfc, 0xb7, 0x00, 0xfd, 0xfc, 0xb7, 0x00, + 0xfd, 0xfd, 0xb7, 0x00, 0xfc, 0xfc, 0xb6, 0x00, 0xfb, 0xfc, 0xb5, 0x00, 0xf4, 0xf4, 0xa5, 0x00, 0xfd, 0xfd, 0xa5, + 0x00, 0xfc, 0xfc, 0x9d, 0x00, 0xfc, 0xfc, 0x94, 0x00, 0xfb, 0xfb, 0x87, 0x00, 0xfb, 0xfb, 0x7b, 0x00, 0xfa, 0xfa, + 0x6e, 0x00, 0xfa, 0xfa, 0x61, 0x00, 0xfa, 0xf7, 0x58, 0x00, 0xfa, 0xf5, 0x4e, 0x00, 0xf7, 0xee, 0x44, 0x00, 0xf3, + 0xe7, 0x3a, 0x00, 0xed, 0xe1, 0x2c, 0x00, 0xe7, 0xdb, 0x1e, 0x00, 0xff, 0xd2, 0x1a, 0x00, 0x78, 0xb0, 0x90, 0x00, + 0x09, 0xa0, 0xfd, 0x00, 0x15, 0x9d, 0xfd, 0x00, 0x18, 0xa0, 0xf8, 0x00, 0x1a, 0xa2, 0xf2, 0x00, 0x18, 0xa0, 0xf2, + 0x00, 0x16, 0x9e, 0xf2, 0x00, 0x13, 0x9b, 0xf2, 0x00, 0x10, 0x99, 0xf1, 0x00, 0x11, 0x9a, 0xf2, 0x00, 0x12, 0x9b, + 0xf3, 0x00, 0x12, 0x9b, 0xf3, 0x00, 0x60, 0xd4, 0xf7, 0x00, 0x67, 0xdc, 0xfd, 0x00, 0x4f, 0xc2, 0xf0, 0x00, 0x00, + 0x2c, 0x8a, 0x00, 0x2e, 0x6b, 0xc0, 0x00, 0x05, 0x47, 0xad, 0x00, 0x00, 0x44, 0xba, 0x00, 0x36, 0x85, 0xc4, 0x00, + 0x06, 0x4e, 0xbc, 0x00, 0x14, 0x62, 0xc3, 0x00, 0x2d, 0x70, 0xcb, 0x00, 0x0f, 0x5a, 0xb4, 0x00, 0x22, 0x74, 0xcd, + 0x00, 0x11, 0x69, 0xc2, 0x00, 0x19, 0x79, 0xc2, 0x00, 0x1d, 0x80, 0xd0, 0x00, 0x19, 0x80, 0xd7, 0x00, 0x1a, 0x86, + 0xd3, 0x00, 0x10, 0x90, 0xde, 0x00, 0x03, 0x8d, 0xda, 0x00, 0x05, 0x99, 0xe6, 0x00, 0x05, 0x9c, 0xe1, 0x00, 0x04, + 0x9e, 0xdd, 0x00, 0x05, 0xa6, 0xe1, 0x00, 0x00, 0xa7, 0xde, 0x00, 0x1f, 0xb6, 0xee, 0x00, 0x39, 0xbd, 0xf7, 0x00, + 0x38, 0xbc, 0xf6, 0x00, 0x24, 0xb5, 0xfc, 0x00, 0xbf, 0xe8, 0xb9, 0x00, 0xfa, 0xfe, 0xa2, 0x00, 0xfb, 0xfc, 0xa5, + 0x00, 0xfc, 0xfa, 0xa8, 0x00, 0xfc, 0xfc, 0xa7, 0x00, 0xfd, 0xfd, 0xa6, 0x00, 0xfb, 0xfc, 0xa3, 0x00, 0xf9, 0xfb, + 0x9f, 0x00, 0xf6, 0xf7, 0x95, 0x00, 0xfa, 0xfb, 0x92, 0x00, 0xfb, 0xfb, 0x8b, 0x00, 0xfb, 0xfb, 0x85, 0x00, 0xfa, + 0xfa, 0x79, 0x00, 0xfa, 0xfa, 0x6d, 0x00, 0xf9, 0xf9, 0x61, 0x00, 0xf8, 0xf9, 0x56, 0x00, 0xf9, 0xf6, 0x4c, 0x00, + 0xf9, 0xf4, 0x42, 0x00, 0xf5, 0xec, 0x39, 0x00, 0xf2, 0xe5, 0x31, 0x00, 0xef, 0xde, 0x28, 0x00, 0xec, 0xd6, 0x20, + 0x00, 0xee, 0xd9, 0x00, 0x00, 0x32, 0xa6, 0xe5, 0x00, 0x19, 0xa4, 0xff, 0x00, 0x29, 0xa4, 0xf4, 0x00, 0x20, 0xa2, + 0xf4, 0x00, 0x18, 0xa0, 0xf5, 0x00, 0x17, 0x9e, 0xf4, 0x00, 0x15, 0x9d, 0xf4, 0x00, 0x13, 0x9b, 0xf3, 0x00, 0x11, + 0x99, 0xf2, 0x00, 0x12, 0x9a, 0xf2, 0x00, 0x12, 0x9a, 0xf3, 0x00, 0x12, 0x9a, 0xf3, 0x00, 0x5b, 0xd1, 0xf5, 0x00, + 0x63, 0xdf, 0xfa, 0x00, 0x31, 0x8d, 0xcc, 0x00, 0x06, 0x2d, 0x91, 0x00, 0x0e, 0x49, 0x9a, 0x00, 0x00, 0x36, 0x9f, + 0x00, 0x00, 0x38, 0x97, 0x00, 0x15, 0x5f, 0xb6, 0x00, 0x53, 0xaa, 0xd9, 0x00, 0x31, 0xa6, 0xe2, 0x00, 0x45, 0xbc, + 0xef, 0x00, 0x6d, 0xdd, 0xff, 0x00, 0x76, 0xde, 0xfa, 0x00, 0x6d, 0xd9, 0xf9, 0x00, 0x64, 0xd5, 0xf9, 0x00, 0x54, + 0xc5, 0xf3, 0x00, 0x45, 0xb5, 0xed, 0x00, 0x23, 0x8e, 0xd6, 0x00, 0x12, 0x77, 0xce, 0x00, 0x00, 0x6c, 0xc6, 0x00, + 0x02, 0x82, 0xde, 0x00, 0x01, 0x87, 0xdb, 0x00, 0x00, 0x8d, 0xd7, 0x00, 0x07, 0x9b, 0xe1, 0x00, 0x00, 0x99, 0xdc, + 0x00, 0x22, 0xb1, 0xf0, 0x00, 0x36, 0xba, 0xf4, 0x00, 0x3c, 0xbc, 0xf4, 0x00, 0x1c, 0xb5, 0xff, 0x00, 0xff, 0xfe, + 0x89, 0x00, 0xfb, 0xff, 0x96, 0x00, 0xfb, 0xfc, 0x98, 0x00, 0xfb, 0xf9, 0x9a, 0x00, 0xfc, 0xfb, 0x98, 0x00, 0xfd, + 0xfd, 0x96, 0x00, 0xfa, 0xfb, 0x90, 0x00, 0xf6, 0xf9, 0x8a, 0x00, 0xf7, 0xf9, 0x84, 0x00, 0xf8, 0xfa, 0x7f, 0x00, + 0xfa, 0xfa, 0x7a, 0x00, 0xfb, 0xfb, 0x75, 0x00, 0xfa, 0xfa, 0x6a, 0x00, 0xf9, 0xf9, 0x60, 0x00, 0xf8, 0xf8, 0x55, + 0x00, 0xf7, 0xf8, 0x4a, 0x00, 0xf7, 0xf5, 0x40, 0x00, 0xf8, 0xf3, 0x36, 0x00, 0xf4, 0xeb, 0x2f, 0x00, 0xf0, 0xe3, + 0x28, 0x00, 0xf0, 0xda, 0x24, 0x00, 0xf0, 0xd1, 0x21, 0x00, 0xe9, 0xca, 0x24, 0x00, 0x04, 0x9b, 0xff, 0x00, 0x20, + 0xa3, 0xf6, 0x00, 0x16, 0xa1, 0xf7, 0x00, 0x16, 0xa0, 0xf7, 0x00, 0x16, 0x9e, 0xf7, 0x00, 0x15, 0x9d, 0xf6, 0x00, + 0x14, 0x9c, 0xf5, 0x00, 0x13, 0x9b, 0xf4, 0x00, 0x12, 0x9a, 0xf3, 0x00, 0x12, 0x9a, 0xf3, 0x00, 0x12, 0x9a, 0xf3, + 0x00, 0x12, 0x9a, 0xf3, 0x00, 0x5a, 0xe3, 0xff, 0x00, 0x64, 0xd8, 0xff, 0x00, 0x0d, 0x47, 0x98, 0x00, 0x00, 0x26, + 0x82, 0x00, 0x1d, 0x6b, 0xb7, 0x00, 0x3a, 0xa2, 0xde, 0x00, 0x5f, 0xe5, 0xff, 0x00, 0x52, 0xd8, 0xfd, 0x00, 0x4d, + 0xd6, 0xf6, 0x00, 0x48, 0xcc, 0xf5, 0x00, 0x5f, 0xd0, 0xf6, 0x00, 0x68, 0xd9, 0xff, 0x00, 0x61, 0xd3, 0xf8, 0x00, + 0x5b, 0xd2, 0xf8, 0x00, 0x42, 0xcb, 0xff, 0x00, 0x53, 0xce, 0xfe, 0x00, 0x51, 0xcf, 0xf5, 0x00, 0x49, 0xca, 0xf6, + 0x00, 0x4a, 0xcd, 0xff, 0x00, 0x40, 0xba, 0xff, 0x00, 0x0e, 0x7e, 0xdb, 0x00, 0x00, 0x69, 0xc2, 0x00, 0x05, 0x84, + 0xda, 0x00, 0x01, 0x84, 0xd5, 0x00, 0x06, 0x8c, 0xd8, 0x00, 0x38, 0xbe, 0xf8, 0x00, 0x3a, 0xbe, 0xf7, 0x00, 0x35, + 0xbe, 0xff, 0x00, 0x62, 0xc7, 0xe2, 0x00, 0xfb, 0xf3, 0x79, 0x00, 0xf8, 0xfa, 0x83, 0x00, 0xf9, 0xf9, 0x83, 0x00, + 0xfa, 0xf8, 0x84, 0x00, 0xf9, 0xf7, 0x7f, 0x00, 0xf7, 0xf7, 0x7b, 0x00, 0xf8, 0xf9, 0x79, 0x00, 0xf9, 0xfa, 0x77, + 0x00, 0xf8, 0xf9, 0x72, 0x00, 0xf7, 0xf8, 0x6c, 0x00, 0xfc, 0xfc, 0x6c, 0x00, 0xf9, 0xf8, 0x64, 0x00, 0xf8, 0xf8, + 0x5b, 0x00, 0xf8, 0xf7, 0x52, 0x00, 0xf7, 0xf6, 0x49, 0x00, 0xf6, 0xf5, 0x3f, 0x00, 0xf5, 0xf2, 0x37, 0x00, 0xf4, + 0xef, 0x2f, 0x00, 0xf1, 0xe6, 0x28, 0x00, 0xee, 0xde, 0x20, 0x00, 0xea, 0xd6, 0x1f, 0x00, 0xf2, 0xcc, 0x11, 0x00, + 0x9d, 0xb9, 0x6c, 0x00, 0x0c, 0x9f, 0xfe, 0x00, 0x1b, 0xa3, 0xf9, 0x00, 0x17, 0xa2, 0xf9, 0x00, 0x17, 0xa0, 0xf9, + 0x00, 0x16, 0x9e, 0xf8, 0x00, 0x16, 0x9d, 0xf7, 0x00, 0x15, 0x9c, 0xf6, 0x00, 0x14, 0x9b, 0xf5, 0x00, 0x13, 0x9a, + 0xf5, 0x00, 0x13, 0x9a, 0xf5, 0x00, 0x13, 0x9a, 0xf5, 0x00, 0x13, 0x9a, 0xf5, 0x00, 0x60, 0xd8, 0xf9, 0x00, 0x5b, + 0xd9, 0xf8, 0x00, 0x4c, 0xad, 0xd7, 0x00, 0x69, 0xdd, 0xff, 0x00, 0x56, 0xdd, 0xf8, 0x00, 0x55, 0xd6, 0xfc, 0x00, + 0x55, 0xd0, 0xff, 0x00, 0x5c, 0xd5, 0xff, 0x00, 0x53, 0xcb, 0xf2, 0x00, 0x4b, 0xca, 0xf6, 0x00, 0x43, 0xca, 0xfa, + 0x00, 0x47, 0xc9, 0xf8, 0x00, 0x4c, 0xc8, 0xf6, 0x00, 0x5c, 0xcf, 0xf1, 0x00, 0x46, 0xcc, 0xf8, 0x00, 0x55, 0xca, + 0xff, 0x00, 0x3e, 0xc4, 0xfa, 0x00, 0x43, 0xc3, 0xfb, 0x00, 0x48, 0xc2, 0xfd, 0x00, 0x3e, 0xbf, 0xf4, 0x00, 0x44, + 0xcc, 0xfb, 0x00, 0x37, 0xb3, 0xfc, 0x00, 0x0b, 0x7b, 0xdd, 0x00, 0x00, 0x6d, 0xc9, 0x00, 0x0d, 0x80, 0xd4, 0x00, + 0x4e, 0xcc, 0xff, 0x00, 0x3e, 0xc3, 0xfa, 0x00, 0x2e, 0xc2, 0xff, 0x00, 0xa7, 0xde, 0xa8, 0x00, 0xf8, 0xec, 0x5b, + 0x00, 0xf5, 0xf5, 0x70, 0x00, 0xf7, 0xf6, 0x6f, 0x00, 0xfa, 0xf7, 0x6e, 0x00, 0xf5, 0xf4, 0x67, 0x00, 0xf1, 0xf0, + 0x60, 0x00, 0xf6, 0xf6, 0x63, 0x00, 0xfb, 0xfc, 0x65, 0x00, 0xf8, 0xf9, 0x5f, 0x00, 0xf6, 0xf6, 0x59, 0x00, 0xfe, + 0xfe, 0x5d, 0x00, 0xf7, 0xf6, 0x52, 0x00, 0xf7, 0xf5, 0x4c, 0x00, 0xf7, 0xf5, 0x45, 0x00, 0xf6, 0xf3, 0x3d, 0x00, + 0xf6, 0xf2, 0x35, 0x00, 0xf3, 0xef, 0x2f, 0x00, 0xf1, 0xeb, 0x29, 0x00, 0xef, 0xe2, 0x21, 0x00, 0xec, 0xd8, 0x18, + 0x00, 0xe5, 0xd2, 0x1a, 0x00, 0xf3, 0xc7, 0x00, 0x00, 0x52, 0xa9, 0xb4, 0x00, 0x14, 0xa4, 0xfb, 0x00, 0x15, 0xa3, + 0xfb, 0x00, 0x17, 0xa3, 0xfc, 0x00, 0x17, 0xa1, 0xfa, 0x00, 0x17, 0x9f, 0xf8, 0x00, 0x16, 0x9d, 0xf8, 0x00, 0x15, + 0x9c, 0xf7, 0x00, 0x15, 0x9b, 0xf7, 0x00, 0x14, 0x99, 0xf6, 0x00, 0x14, 0x99, 0xf6, 0x00, 0x14, 0x99, 0xf6, 0x00, + 0x14, 0x99, 0xf6, 0x00, 0x58, 0xcf, 0xf2, 0x00, 0x59, 0xdd, 0xfd, 0x00, 0x55, 0xd5, 0xf9, 0x00, 0x5d, 0xde, 0xff, + 0x00, 0x4d, 0xce, 0xf3, 0x00, 0x4d, 0xcb, 0xf3, 0x00, 0x4c, 0xc8, 0xf3, 0x00, 0x56, 0xd2, 0xfc, 0x00, 0x59, 0xd3, + 0xfd, 0x00, 0x50, 0xce, 0xfb, 0x00, 0x47, 0xca, 0xfa, 0x00, 0x48, 0xc9, 0xf9, 0x00, 0x49, 0xc7, 0xf9, 0x00, 0x51, + 0xcb, 0xf6, 0x00, 0x45, 0xc9, 0xf9, 0x00, 0x4b, 0xc8, 0xfd, 0x00, 0x3f, 0xc5, 0xf9, 0x00, 0x41, 0xc4, 0xfa, 0x00, + 0x43, 0xc2, 0xfb, 0x00, 0x3b, 0xbd, 0xf3, 0x00, 0x3a, 0xc0, 0xf4, 0x00, 0x3e, 0xc7, 0xfc, 0x00, 0x3a, 0xc6, 0xfc, + 0x00, 0x25, 0xa1, 0xe3, 0x00, 0x1f, 0x8d, 0xd9, 0x00, 0x37, 0xb9, 0xf7, 0x00, 0x26, 0xbb, 0xfa, 0x00, 0x2a, 0xbb, + 0xf4, 0x00, 0xce, 0xd8, 0x57, 0x00, 0xf9, 0xfa, 0x5b, 0x00, 0xd9, 0xdb, 0x49, 0x00, 0xed, 0xec, 0x58, 0x00, 0xfa, + 0xf5, 0x60, 0x00, 0xf2, 0xef, 0x4d, 0x00, 0xe9, 0xea, 0x3b, 0x00, 0xee, 0xef, 0x46, 0x00, 0xf2, 0xf4, 0x51, 0x00, + 0xf9, 0xf3, 0x4f, 0x00, 0xed, 0xf1, 0x45, 0x00, 0xfe, 0xf8, 0x4b, 0x00, 0xf4, 0xf5, 0x42, 0x00, 0xf5, 0xf4, 0x3d, + 0x00, 0xf6, 0xf3, 0x37, 0x00, 0xf5, 0xf1, 0x31, 0x00, 0xf5, 0xef, 0x2b, 0x00, 0xf2, 0xeb, 0x27, 0x00, 0xf0, 0xe6, + 0x22, 0x00, 0xee, 0xdb, 0x1d, 0x00, 0xec, 0xd1, 0x17, 0x00, 0xf1, 0xcc, 0x09, 0x00, 0xf5, 0xc5, 0x09, 0x00, 0x0f, + 0xad, 0xff, 0x00, 0x17, 0xa1, 0xf9, 0x00, 0x18, 0xa1, 0xf9, 0x00, 0x18, 0xa1, 0xf8, 0x00, 0x18, 0xa0, 0xf9, 0x00, + 0x17, 0x9f, 0xf9, 0x00, 0x16, 0x9d, 0xf9, 0x00, 0x16, 0x9c, 0xf8, 0x00, 0x15, 0x9b, 0xf8, 0x00, 0x15, 0x99, 0xf8, + 0x00, 0x15, 0x99, 0xf8, 0x00, 0x15, 0x99, 0xf8, 0x00, 0x15, 0x99, 0xf8, 0x00, 0x60, 0xd5, 0xfb, 0x00, 0x5b, 0xd3, + 0xfb, 0x00, 0x56, 0xd2, 0xfb, 0x00, 0x55, 0xd1, 0xfc, 0x00, 0x55, 0xd0, 0xfe, 0x00, 0x54, 0xd0, 0xfa, 0x00, 0x53, + 0xd1, 0xf6, 0x00, 0x51, 0xce, 0xf7, 0x00, 0x4e, 0xcb, 0xf8, 0x00, 0x4d, 0xcb, 0xf9, 0x00, 0x4c, 0xca, 0xfb, 0x00, + 0x49, 0xc8, 0xfb, 0x00, 0x47, 0xc6, 0xfc, 0x00, 0x45, 0xc6, 0xfb, 0x00, 0x43, 0xc6, 0xfa, 0x00, 0x41, 0xc6, 0xfa, + 0x00, 0x40, 0xc7, 0xf9, 0x00, 0x3f, 0xc5, 0xf9, 0x00, 0x3e, 0xc3, 0xf9, 0x00, 0x3f, 0xc3, 0xfb, 0x00, 0x41, 0xc4, + 0xfd, 0x00, 0x38, 0xba, 0xf2, 0x00, 0x40, 0xc1, 0xf8, 0x00, 0x3d, 0xc3, 0xfb, 0x00, 0x3b, 0xc5, 0xfe, 0x00, 0x37, + 0xc1, 0xf6, 0x00, 0x34, 0xbe, 0xef, 0x00, 0x2e, 0xbc, 0xf0, 0x00, 0xde, 0xd7, 0x22, 0x00, 0xbf, 0xdc, 0x38, 0x00, + 0xde, 0xe1, 0x42, 0x00, 0xec, 0xea, 0x4a, 0x00, 0xea, 0xe4, 0x42, 0x00, 0xee, 0xe9, 0x42, 0x00, 0xf2, 0xee, 0x42, + 0x00, 0xee, 0xed, 0x3f, 0x00, 0xea, 0xec, 0x3d, 0x00, 0xfb, 0xee, 0x3f, 0x00, 0xe5, 0xec, 0x31, 0x00, 0xff, 0xf2, + 0x39, 0x00, 0xf2, 0xf5, 0x31, 0x00, 0xf4, 0xf3, 0x2e, 0x00, 0xf5, 0xf1, 0x2a, 0x00, 0xf5, 0xee, 0x25, 0x00, 0xf4, + 0xec, 0x21, 0x00, 0xf2, 0xe7, 0x1e, 0x00, 0xf0, 0xe1, 0x1c, 0x00, 0xee, 0xd5, 0x19, 0x00, 0xec, 0xc9, 0x17, 0x00, + 0xde, 0xc4, 0x0c, 0x00, 0xbb, 0xbe, 0x39, 0x00, 0x07, 0x98, 0xf8, 0x00, 0x1a, 0x9f, 0xf8, 0x00, 0x1a, 0x9f, 0xf7, + 0x00, 0x1a, 0x9f, 0xf5, 0x00, 0x18, 0x9f, 0xf7, 0x00, 0x17, 0x9f, 0xf9, 0x00, 0x17, 0x9e, 0xf9, 0x00, 0x16, 0x9c, + 0xf9, 0x00, 0x16, 0x9b, 0xf9, 0x00, 0x16, 0x99, 0xf9, 0x00, 0x16, 0x99, 0xf9, 0x00, 0x16, 0x99, 0xf9, 0x00, 0x16, + 0x99, 0xf9, 0x00, 0x5c, 0xd4, 0xf9, 0x00, 0x58, 0xd4, 0xf9, 0x00, 0x55, 0xd3, 0xf9, 0x00, 0x56, 0xd2, 0xfa, 0x00, + 0x58, 0xd0, 0xfb, 0x00, 0x56, 0xd0, 0xf8, 0x00, 0x54, 0xd0, 0xf6, 0x00, 0x51, 0xce, 0xf7, 0x00, 0x4d, 0xcc, 0xf9, + 0x00, 0x4c, 0xcb, 0xfa, 0x00, 0x4b, 0xca, 0xfb, 0x00, 0x49, 0xc8, 0xfb, 0x00, 0x47, 0xc7, 0xfb, 0x00, 0x45, 0xc7, + 0xfb, 0x00, 0x43, 0xc6, 0xfa, 0x00, 0x41, 0xc6, 0xfa, 0x00, 0x40, 0xc6, 0xf9, 0x00, 0x3f, 0xc4, 0xf9, 0x00, 0x3e, + 0xc3, 0xf9, 0x00, 0x3e, 0xc2, 0xfa, 0x00, 0x3e, 0xc2, 0xfb, 0x00, 0x3a, 0xbe, 0xf5, 0x00, 0x3e, 0xc2, 0xf8, 0x00, + 0x3b, 0xc1, 0xf9, 0x00, 0x37, 0xc0, 0xf9, 0x00, 0x36, 0xbe, 0xff, 0x00, 0x35, 0xbb, 0xff, 0x00, 0x67, 0xbb, 0x84, + 0x00, 0xb0, 0xd2, 0x19, 0x00, 0xb4, 0xd3, 0x1a, 0x00, 0xd3, 0xda, 0x39, 0x00, 0xe2, 0xdd, 0x3d, 0x00, 0xd6, 0xd5, + 0x32, 0x00, 0xe1, 0xdf, 0x38, 0x00, 0xec, 0xe9, 0x3e, 0x00, 0xe1, 0xe6, 0x36, 0x00, 0xe9, 0xe5, 0x36, 0x00, 0xf1, + 0xe6, 0x34, 0x00, 0xe5, 0xe4, 0x2b, 0x00, 0xf6, 0xe6, 0x2e, 0x00, 0xe9, 0xeb, 0x29, 0x00, 0xf0, 0xee, 0x2a, 0x00, + 0xf0, 0xe8, 0x24, 0x00, 0xec, 0xe4, 0x20, 0x00, 0xe9, 0xe0, 0x1d, 0x00, 0xeb, 0xdb, 0x1c, 0x00, 0xed, 0xd7, 0x1c, + 0x00, 0xe9, 0xce, 0x19, 0x00, 0xe5, 0xc5, 0x16, 0x00, 0xe7, 0xc0, 0x04, 0x00, 0x6c, 0xb2, 0x92, 0x00, 0x10, 0x9d, + 0xfc, 0x00, 0x18, 0xa1, 0xf7, 0x00, 0x1a, 0xa0, 0xf5, 0x00, 0x1c, 0xa0, 0xf3, 0x00, 0x19, 0xa0, 0xf6, 0x00, 0x17, + 0x9f, 0xf9, 0x00, 0x16, 0x9e, 0xf9, 0x00, 0x16, 0x9c, 0xf9, 0x00, 0x15, 0x9b, 0xf8, 0x00, 0x15, 0x9a, 0xf8, 0x00, + 0x14, 0x99, 0xf8, 0x00, 0x14, 0x99, 0xf7, 0x00, 0x14, 0x99, 0xf7, 0x00, 0x58, 0xd4, 0xf6, 0x00, 0x56, 0xd4, 0xf6, + 0x00, 0x54, 0xd5, 0xf7, 0x00, 0x57, 0xd3, 0xf7, 0x00, 0x5b, 0xd1, 0xf8, 0x00, 0x58, 0xd0, 0xf6, 0x00, 0x54, 0xcf, + 0xf5, 0x00, 0x50, 0xce, 0xf8, 0x00, 0x4d, 0xcd, 0xfa, 0x00, 0x4b, 0xcb, 0xfb, 0x00, 0x4a, 0xca, 0xfb, 0x00, 0x48, + 0xc9, 0xfb, 0x00, 0x46, 0xc7, 0xfb, 0x00, 0x45, 0xc7, 0xfa, 0x00, 0x43, 0xc7, 0xfa, 0x00, 0x42, 0xc6, 0xfa, 0x00, + 0x40, 0xc6, 0xf9, 0x00, 0x3f, 0xc4, 0xf9, 0x00, 0x3e, 0xc3, 0xf9, 0x00, 0x3d, 0xc1, 0xf9, 0x00, 0x3c, 0xc0, 0xf9, + 0x00, 0x3c, 0xc1, 0xf8, 0x00, 0x3c, 0xc2, 0xf7, 0x00, 0x38, 0xbf, 0xf6, 0x00, 0x34, 0xbb, 0xf5, 0x00, 0x35, 0xbd, + 0xfd, 0x00, 0x37, 0xbe, 0xff, 0x00, 0x46, 0xbc, 0xfc, 0x00, 0x82, 0xc9, 0x2c, 0x00, 0xa0, 0xbe, 0x02, 0x00, 0xb8, + 0xc4, 0x20, 0x00, 0xd8, 0xcf, 0x31, 0x00, 0xd2, 0xd6, 0x32, 0x00, 0xd4, 0xd5, 0x2e, 0x00, 0xd7, 0xd4, 0x2a, 0x00, + 0xcd, 0xd7, 0x25, 0x00, 0xe9, 0xdf, 0x2f, 0x00, 0xe6, 0xdd, 0x2a, 0x00, 0xe4, 0xdc, 0x25, 0x00, 0xed, 0xd9, 0x22, + 0x00, 0xe0, 0xe2, 0x20, 0x00, 0xed, 0xe9, 0x27, 0x00, 0xea, 0xe0, 0x1e, 0x00, 0xe4, 0xda, 0x1c, 0x00, 0xde, 0xd3, + 0x19, 0x00, 0xe5, 0xd0, 0x1a, 0x00, 0xeb, 0xcd, 0x1b, 0x00, 0xe5, 0xc8, 0x18, 0x00, 0xde, 0xc2, 0x14, 0x00, 0xf0, + 0xbc, 0x00, 0x00, 0x1d, 0xa5, 0xeb, 0x00, 0x19, 0xa1, 0xff, 0x00, 0x16, 0xa2, 0xf7, 0x00, 0x19, 0xa2, 0xf4, 0x00, + 0x1e, 0xa2, 0xf1, 0x00, 0x1a, 0xa0, 0xf5, 0x00, 0x16, 0x9f, 0xf9, 0x00, 0x16, 0x9e, 0xf8, 0x00, 0x15, 0x9d, 0xf8, + 0x00, 0x15, 0x9c, 0xf8, 0x00, 0x14, 0x9b, 0xf8, 0x00, 0x13, 0x9a, 0xf7, 0x00, 0x12, 0x99, 0xf6, 0x00, 0x12, 0x99, + 0xf6, 0x00, 0x5e, 0xd5, 0xf9, 0x00, 0x63, 0xd6, 0xfc, 0x00, 0x68, 0xd6, 0xff, 0x00, 0x5f, 0xd3, 0xfc, 0x00, 0x56, + 0xd0, 0xf8, 0x00, 0x53, 0xcf, 0xf8, 0x00, 0x51, 0xce, 0xf8, 0x00, 0x4e, 0xcd, 0xf9, 0x00, 0x4b, 0xcc, 0xfb, 0x00, + 0x4a, 0xcb, 0xfb, 0x00, 0x48, 0xca, 0xfb, 0x00, 0x47, 0xc9, 0xfa, 0x00, 0x46, 0xc8, 0xfb, 0x00, 0x44, 0xc7, 0xfa, + 0x00, 0x43, 0xc7, 0xfa, 0x00, 0x42, 0xc6, 0xfa, 0x00, 0x40, 0xc5, 0xf9, 0x00, 0x3f, 0xc4, 0xf9, 0x00, 0x3e, 0xc3, + 0xf9, 0x00, 0x3d, 0xc1, 0xf9, 0x00, 0x3c, 0xc0, 0xf9, 0x00, 0x3b, 0xc1, 0xf9, 0x00, 0x3b, 0xc1, 0xf8, 0x00, 0x38, + 0xbf, 0xf7, 0x00, 0x36, 0xbd, 0xf7, 0x00, 0x35, 0xbd, 0xfa, 0x00, 0x34, 0xbd, 0xfe, 0x00, 0x22, 0xc3, 0xf6, 0x00, + 0x27, 0xbb, 0xfc, 0x00, 0x53, 0xb0, 0xb2, 0x00, 0x9b, 0xc6, 0x06, 0x00, 0xc1, 0xd3, 0x22, 0x00, 0xd3, 0xdd, 0x36, + 0x00, 0xb4, 0xba, 0x12, 0x00, 0xc4, 0xc7, 0x1f, 0x00, 0xc5, 0xcf, 0x22, 0x00, 0xd9, 0xd8, 0x2d, 0x00, 0xdf, 0xdb, + 0x30, 0x00, 0xdc, 0xd5, 0x2b, 0x00, 0xe8, 0xd5, 0x20, 0x00, 0xd5, 0xd5, 0x1c, 0x00, 0xe8, 0xe4, 0x28, 0x00, 0xec, + 0xe3, 0x24, 0x00, 0xd1, 0xce, 0x1f, 0x00, 0xd3, 0xc5, 0x1d, 0x00, 0xdc, 0xc3, 0x02, 0x00, 0xcf, 0xc3, 0x12, 0x00, + 0xe3, 0xc2, 0x09, 0x00, 0xe3, 0xbe, 0x00, 0x00, 0x84, 0xbf, 0x6e, 0x00, 0x0c, 0xa0, 0xf6, 0x00, 0x12, 0x9f, 0xfd, + 0x00, 0x18, 0xa2, 0xf6, 0x00, 0x19, 0xa1, 0xf5, 0x00, 0x1b, 0xa1, 0xf4, 0x00, 0x18, 0xa0, 0xf6, 0x00, 0x16, 0x9f, + 0xf8, 0x00, 0x15, 0x9e, 0xf8, 0x00, 0x15, 0x9d, 0xf8, 0x00, 0x14, 0x9c, 0xf7, 0x00, 0x13, 0x9b, 0xf7, 0x00, 0x12, + 0x9a, 0xf6, 0x00, 0x10, 0x98, 0xf4, 0x00, 0x10, 0x98, 0xf4, 0x00, 0x65, 0xd7, 0xfb, 0x00, 0x5d, 0xd4, 0xfa, 0x00, + 0x56, 0xd2, 0xf8, 0x00, 0x53, 0xd0, 0xf9, 0x00, 0x50, 0xcf, 0xf9, 0x00, 0x4f, 0xce, 0xf9, 0x00, 0x4d, 0xcd, 0xfa, + 0x00, 0x4b, 0xcd, 0xfa, 0x00, 0x4a, 0xcc, 0xfb, 0x00, 0x48, 0xcb, 0xfb, 0x00, 0x47, 0xca, 0xfb, 0x00, 0x46, 0xc9, + 0xfa, 0x00, 0x45, 0xc8, 0xfa, 0x00, 0x44, 0xc7, 0xfa, 0x00, 0x43, 0xc7, 0xfa, 0x00, 0x42, 0xc6, 0xfa, 0x00, 0x40, + 0xc5, 0xfa, 0x00, 0x3f, 0xc4, 0xf9, 0x00, 0x3e, 0xc3, 0xf9, 0x00, 0x3d, 0xc1, 0xf9, 0x00, 0x3b, 0xc0, 0xf9, 0x00, + 0x3a, 0xc0, 0xf9, 0x00, 0x39, 0xc0, 0xf9, 0x00, 0x38, 0xbf, 0xf9, 0x00, 0x37, 0xbf, 0xf9, 0x00, 0x34, 0xbe, 0xf8, + 0x00, 0x31, 0xbc, 0xf7, 0x00, 0x33, 0xbb, 0xf8, 0x00, 0x35, 0xbb, 0xfa, 0x00, 0x2c, 0xbc, 0xff, 0x00, 0x61, 0xc2, + 0xdf, 0x00, 0x93, 0xcb, 0x85, 0x00, 0xc5, 0xd5, 0x2b, 0x00, 0xcb, 0xd8, 0x2f, 0x00, 0xb0, 0xbb, 0x13, 0x00, 0xb5, + 0xbe, 0x17, 0x00, 0xb9, 0xc2, 0x1b, 0x00, 0xc7, 0xc8, 0x26, 0x00, 0xc5, 0xbf, 0x21, 0x00, 0xdb, 0xc8, 0x17, 0x00, + 0xca, 0xc8, 0x19, 0x00, 0xdb, 0xd7, 0x22, 0x00, 0xdd, 0xd6, 0x1a, 0x00, 0xb7, 0xbd, 0x0d, 0x00, 0xc8, 0xbd, 0x04, + 0x00, 0xd0, 0xc0, 0x00, 0x00, 0xad, 0xc9, 0x51, 0x00, 0x6c, 0xb8, 0xb1, 0x00, 0x04, 0xa3, 0xff, 0x00, 0x13, 0xa4, + 0xfb, 0x00, 0x21, 0xa4, 0xf5, 0x00, 0x1e, 0xa3, 0xf5, 0x00, 0x1a, 0xa1, 0xf6, 0x00, 0x19, 0xa1, 0xf6, 0x00, 0x18, + 0xa0, 0xf7, 0x00, 0x17, 0xa0, 0xf7, 0x00, 0x16, 0x9f, 0xf8, 0x00, 0x15, 0x9e, 0xf7, 0x00, 0x14, 0x9e, 0xf7, 0x00, + 0x13, 0x9d, 0xf7, 0x00, 0x13, 0x9c, 0xf6, 0x00, 0x11, 0x9a, 0xf4, 0x00, 0x0f, 0x98, 0xf2, 0x00, 0x0f, 0x98, 0xf2, + 0x00, 0x5c, 0xd5, 0xf9, 0x00, 0x58, 0xd3, 0xf8, 0x00, 0x53, 0xd1, 0xf8, 0x00, 0x52, 0xd0, 0xf9, 0x00, 0x50, 0xcf, + 0xf9, 0x00, 0x4e, 0xce, 0xfa, 0x00, 0x4c, 0xcd, 0xfa, 0x00, 0x4a, 0xcc, 0xfa, 0x00, 0x48, 0xcc, 0xfa, 0x00, 0x47, + 0xcb, 0xfa, 0x00, 0x46, 0xca, 0xfa, 0x00, 0x45, 0xc9, 0xfa, 0x00, 0x44, 0xc8, 0xfa, 0x00, 0x43, 0xc7, 0xfa, 0x00, + 0x42, 0xc7, 0xfa, 0x00, 0x41, 0xc6, 0xfa, 0x00, 0x40, 0xc5, 0xfa, 0x00, 0x3f, 0xc4, 0xf9, 0x00, 0x3e, 0xc2, 0xf9, + 0x00, 0x3c, 0xc1, 0xf9, 0x00, 0x3b, 0xc0, 0xf9, 0x00, 0x3a, 0xc0, 0xf9, 0x00, 0x38, 0xbf, 0xf9, 0x00, 0x37, 0xbf, + 0xf9, 0x00, 0x36, 0xbf, 0xf9, 0x00, 0x35, 0xbd, 0xf6, 0x00, 0x34, 0xbb, 0xf3, 0x00, 0x35, 0xb9, 0xf7, 0x00, 0x35, + 0xb8, 0xfb, 0x00, 0x22, 0xb5, 0xff, 0x00, 0x2f, 0xb5, 0xff, 0x00, 0x4d, 0xba, 0xe6, 0x00, 0x6b, 0xbf, 0xce, 0x00, + 0x27, 0xb1, 0xc5, 0x00, 0x6c, 0xbc, 0x7c, 0x00, 0x8a, 0xbd, 0x49, 0x00, 0xa7, 0xbe, 0x15, 0x00, 0xb9, 0xbf, 0x09, + 0x00, 0xcc, 0xc0, 0x00, 0x00, 0xda, 0xc4, 0x3d, 0x00, 0xbb, 0xca, 0x20, 0x00, 0xae, 0xc7, 0x3e, 0x00, 0x99, 0xbc, + 0x54, 0x00, 0x5a, 0xad, 0x8b, 0x00, 0x36, 0xab, 0xc4, 0x00, 0x04, 0xb3, 0xff, 0x00, 0x15, 0xa7, 0xff, 0x00, 0x21, + 0xa4, 0xff, 0x00, 0x19, 0xa0, 0xfb, 0x00, 0x1b, 0xa2, 0xfa, 0x00, 0x1d, 0xa4, 0xf9, 0x00, 0x1b, 0xa3, 0xf8, 0x00, + 0x1a, 0xa1, 0xf7, 0x00, 0x19, 0xa1, 0xf7, 0x00, 0x18, 0xa0, 0xf7, 0x00, 0x17, 0xa0, 0xf7, 0x00, 0x16, 0x9f, 0xf8, + 0x00, 0x15, 0x9e, 0xf7, 0x00, 0x14, 0x9e, 0xf7, 0x00, 0x13, 0x9d, 0xf7, 0x00, 0x12, 0x9c, 0xf6, 0x00, 0x11, 0x9a, + 0xf5, 0x00, 0x0f, 0x99, 0xf3, 0x00, 0x0f, 0x99, 0xf3, 0x00, 0x53, 0xd2, 0xf6, 0x00, 0x52, 0xd1, 0xf7, 0x00, 0x51, + 0xd1, 0xf8, 0x00, 0x50, 0xd0, 0xf9, 0x00, 0x4f, 0xcf, 0xfa, 0x00, 0x4d, 0xce, 0xfa, 0x00, 0x4b, 0xcd, 0xfa, 0x00, + 0x49, 0xcc, 0xfa, 0x00, 0x47, 0xcb, 0xfa, 0x00, 0x46, 0xca, 0xf9, 0x00, 0x45, 0xca, 0xf9, 0x00, 0x44, 0xc9, 0xf9, + 0x00, 0x44, 0xc8, 0xfa, 0x00, 0x43, 0xc7, 0xfa, 0x00, 0x42, 0xc6, 0xf9, 0x00, 0x41, 0xc6, 0xf9, 0x00, 0x40, 0xc5, + 0xfa, 0x00, 0x3f, 0xc4, 0xf9, 0x00, 0x3d, 0xc2, 0xf9, 0x00, 0x3c, 0xc1, 0xf9, 0x00, 0x3a, 0xc0, 0xf9, 0x00, 0x39, + 0xc0, 0xf9, 0x00, 0x38, 0xbf, 0xf9, 0x00, 0x36, 0xbf, 0xf9, 0x00, 0x35, 0xbe, 0xf8, 0x00, 0x36, 0xbc, 0xf4, 0x00, + 0x38, 0xba, 0xf0, 0x00, 0x36, 0xb8, 0xf6, 0x00, 0x34, 0xb5, 0xfc, 0x00, 0x2c, 0xb6, 0xf9, 0x00, 0x23, 0xb7, 0xf6, + 0x00, 0x25, 0xb5, 0xfa, 0x00, 0x28, 0xb4, 0xff, 0x00, 0x28, 0xb6, 0xff, 0x00, 0x29, 0xb7, 0xff, 0x00, 0x1f, 0xb5, + 0xff, 0x00, 0x15, 0xb2, 0xff, 0x00, 0x20, 0xae, 0xf7, 0x00, 0x3c, 0xb9, 0xff, 0x00, 0x5a, 0xcb, 0xf0, 0x00, 0x42, + 0xbe, 0xfa, 0x00, 0x2a, 0xb6, 0xfc, 0x00, 0x12, 0xad, 0xff, 0x00, 0x18, 0xac, 0xfc, 0x00, 0x1e, 0xac, 0xfa, 0x00, + 0x1e, 0xa9, 0xfd, 0x00, 0x1e, 0xa7, 0xff, 0x00, 0x1b, 0xa8, 0xfa, 0x00, 0x18, 0xa8, 0xf4, 0x00, 0x18, 0xa6, 0xf8, + 0x00, 0x18, 0xa4, 0xfd, 0x00, 0x19, 0xa3, 0xfa, 0x00, 0x1a, 0xa1, 0xf7, 0x00, 0x19, 0xa1, 0xf7, 0x00, 0x18, 0xa0, + 0xf8, 0x00, 0x17, 0xa0, 0xf8, 0x00, 0x16, 0x9f, 0xf8, 0x00, 0x15, 0x9e, 0xf7, 0x00, 0x14, 0x9d, 0xf7, 0x00, 0x13, + 0x9c, 0xf6, 0x00, 0x12, 0x9b, 0xf6, 0x00, 0x11, 0x9a, 0xf5, 0x00, 0x10, 0x99, 0xf4, 0x00, 0x10, 0x99, 0xf4, 0x00, + 0x54, 0xd1, 0xf8, 0x00, 0x52, 0xd1, 0xf8, 0x00, 0x51, 0xd0, 0xf9, 0x00, 0x4f, 0xcf, 0xf9, 0x00, 0x4e, 0xcf, 0xfa, + 0x00, 0x4c, 0xce, 0xfa, 0x00, 0x4a, 0xcd, 0xf9, 0x00, 0x48, 0xcc, 0xf9, 0x00, 0x45, 0xcb, 0xf9, 0x00, 0x45, 0xca, + 0xf9, 0x00, 0x44, 0xc9, 0xf9, 0x00, 0x43, 0xc8, 0xf9, 0x00, 0x43, 0xc8, 0xf9, 0x00, 0x42, 0xc7, 0xf9, 0x00, 0x42, + 0xc6, 0xf9, 0x00, 0x41, 0xc5, 0xf9, 0x00, 0x40, 0xc5, 0xfa, 0x00, 0x3f, 0xc4, 0xf9, 0x00, 0x3d, 0xc2, 0xf9, 0x00, + 0x3b, 0xc1, 0xf9, 0x00, 0x3a, 0xc0, 0xfa, 0x00, 0x38, 0xbf, 0xf9, 0x00, 0x37, 0xbf, 0xf9, 0x00, 0x36, 0xbe, 0xf9, + 0x00, 0x34, 0xbe, 0xf8, 0x00, 0x35, 0xbc, 0xf6, 0x00, 0x35, 0xba, 0xf5, 0x00, 0x34, 0xb8, 0xf8, 0x00, 0x33, 0xb6, + 0xfc, 0x00, 0x2e, 0xb6, 0xf9, 0x00, 0x29, 0xb6, 0xf7, 0x00, 0x29, 0xb5, 0xf8, 0x00, 0x2a, 0xb4, 0xfa, 0x00, 0x2a, + 0xb5, 0xfb, 0x00, 0x2a, 0xb5, 0xfc, 0x00, 0x2a, 0xb2, 0xf6, 0x00, 0x2a, 0xaf, 0xef, 0x00, 0x1b, 0xa9, 0xf6, 0x00, + 0x9b, 0xcf, 0xd9, 0x00, 0x6d, 0xcf, 0xe9, 0x00, 0x74, 0xc7, 0xe4, 0x00, 0x80, 0xc9, 0xdd, 0x00, 0x19, 0xad, 0xfb, + 0x00, 0x1c, 0xac, 0xf9, 0x00, 0x1f, 0xab, 0xf8, 0x00, 0x1f, 0xa9, 0xf9, 0x00, 0x1e, 0xa7, 0xfb, 0x00, 0x1c, 0xa7, + 0xf9, 0x00, 0x1a, 0xa7, 0xf6, 0x00, 0x1a, 0xa5, 0xf8, 0x00, 0x1a, 0xa4, 0xfb, 0x00, 0x1a, 0xa3, 0xfa, 0x00, 0x1a, + 0xa2, 0xf8, 0x00, 0x19, 0xa1, 0xf8, 0x00, 0x18, 0xa0, 0xf8, 0x00, 0x17, 0xa0, 0xf8, 0x00, 0x16, 0x9f, 0xf8, 0x00, + 0x15, 0x9e, 0xf7, 0x00, 0x14, 0x9d, 0xf7, 0x00, 0x13, 0x9c, 0xf6, 0x00, 0x12, 0x9b, 0xf6, 0x00, 0x11, 0x9b, 0xf5, + 0x00, 0x11, 0x9a, 0xf5, 0x00, 0x11, 0x9a, 0xf5, 0x00, 0x55, 0xd0, 0xf9, 0x00, 0x53, 0xd0, 0xfa, 0x00, 0x51, 0xd0, + 0xfa, 0x00, 0x4f, 0xcf, 0xfa, 0x00, 0x4d, 0xcf, 0xfa, 0x00, 0x4b, 0xce, 0xfa, 0x00, 0x49, 0xcd, 0xf9, 0x00, 0x46, + 0xcc, 0xf9, 0x00, 0x44, 0xca, 0xf8, 0x00, 0x43, 0xca, 0xf8, 0x00, 0x43, 0xc9, 0xf8, 0x00, 0x43, 0xc8, 0xf9, 0x00, + 0x42, 0xc8, 0xf9, 0x00, 0x42, 0xc7, 0xf9, 0x00, 0x41, 0xc6, 0xf9, 0x00, 0x41, 0xc6, 0xf9, 0x00, 0x40, 0xc5, 0xfa, + 0x00, 0x3e, 0xc3, 0xf9, 0x00, 0x3d, 0xc2, 0xfa, 0x00, 0x3b, 0xc1, 0xfa, 0x00, 0x39, 0xc0, 0xfa, 0x00, 0x38, 0xbf, + 0xf9, 0x00, 0x36, 0xbf, 0xf9, 0x00, 0x35, 0xbe, 0xf9, 0x00, 0x34, 0xbd, 0xf8, 0x00, 0x33, 0xbc, 0xf9, 0x00, 0x33, + 0xba, 0xfa, 0x00, 0x32, 0xb9, 0xfb, 0x00, 0x32, 0xb8, 0xfc, 0x00, 0x30, 0xb7, 0xfa, 0x00, 0x2e, 0xb6, 0xf8, 0x00, + 0x2d, 0xb5, 0xf7, 0x00, 0x2b, 0xb4, 0xf5, 0x00, 0x2b, 0xb4, 0xf6, 0x00, 0x2b, 0xb3, 0xf7, 0x00, 0x29, 0xb2, 0xf9, + 0x00, 0x28, 0xb2, 0xfc, 0x00, 0x30, 0xb2, 0xf7, 0x00, 0x12, 0xa8, 0xfe, 0x00, 0x7f, 0xd4, 0xe1, 0x00, 0x58, 0xbb, + 0xe6, 0x00, 0x15, 0xaa, 0xfb, 0x00, 0x1f, 0xad, 0xf8, 0x00, 0x20, 0xac, 0xf7, 0x00, 0x20, 0xaa, 0xf5, 0x00, 0x1f, + 0xa9, 0xf6, 0x00, 0x1e, 0xa8, 0xf7, 0x00, 0x1d, 0xa6, 0xf7, 0x00, 0x1c, 0xa5, 0xf8, 0x00, 0x1c, 0xa4, 0xf8, 0x00, + 0x1b, 0xa3, 0xf9, 0x00, 0x1b, 0xa3, 0xf9, 0x00, 0x1b, 0xa2, 0xf9, 0x00, 0x19, 0xa1, 0xf9, 0x00, 0x18, 0xa0, 0xf8, + 0x00, 0x17, 0xa0, 0xf8, 0x00, 0x16, 0x9f, 0xf8, 0x00, 0x15, 0x9e, 0xf7, 0x00, 0x14, 0x9d, 0xf7, 0x00, 0x13, 0x9c, + 0xf6, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x55, + 0xd0, 0xf9, 0x00, 0x53, 0xd0, 0xfa, 0x00, 0x51, 0xd0, 0xfa, 0x00, 0x4f, 0xcf, 0xfa, 0x00, 0x4d, 0xcf, 0xfa, 0x00, + 0x4b, 0xce, 0xfa, 0x00, 0x49, 0xcd, 0xf9, 0x00, 0x46, 0xcc, 0xf9, 0x00, 0x44, 0xca, 0xf8, 0x00, 0x43, 0xca, 0xf8, + 0x00, 0x43, 0xc9, 0xf8, 0x00, 0x43, 0xc8, 0xf9, 0x00, 0x42, 0xc8, 0xf9, 0x00, 0x42, 0xc7, 0xf9, 0x00, 0x41, 0xc6, + 0xf9, 0x00, 0x41, 0xc6, 0xf9, 0x00, 0x40, 0xc5, 0xfa, 0x00, 0x3e, 0xc3, 0xf9, 0x00, 0x3d, 0xc2, 0xfa, 0x00, 0x3b, + 0xc1, 0xfa, 0x00, 0x39, 0xc0, 0xfa, 0x00, 0x38, 0xbf, 0xf9, 0x00, 0x36, 0xbf, 0xf9, 0x00, 0x35, 0xbe, 0xf9, 0x00, + 0x34, 0xbd, 0xf8, 0x00, 0x33, 0xbc, 0xf9, 0x00, 0x33, 0xba, 0xfa, 0x00, 0x32, 0xb9, 0xfb, 0x00, 0x32, 0xb8, 0xfc, + 0x00, 0x30, 0xb7, 0xfa, 0x00, 0x2e, 0xb6, 0xf8, 0x00, 0x2d, 0xb5, 0xf7, 0x00, 0x2b, 0xb4, 0xf5, 0x00, 0x2b, 0xb4, + 0xf6, 0x00, 0x2b, 0xb3, 0xf7, 0x00, 0x2a, 0xb2, 0xf8, 0x00, 0x29, 0xb2, 0xfa, 0x00, 0x2d, 0xb6, 0xf5, 0x00, 0x1d, + 0xb5, 0xf6, 0x00, 0x23, 0x9b, 0xff, 0x00, 0x20, 0xb6, 0xf3, 0x00, 0x0c, 0xac, 0xfb, 0x00, 0x1e, 0xac, 0xf7, 0x00, + 0x1f, 0xab, 0xf6, 0x00, 0x20, 0xaa, 0xf5, 0x00, 0x1f, 0xa9, 0xf6, 0x00, 0x1e, 0xa8, 0xf7, 0x00, 0x1d, 0xa6, 0xf7, + 0x00, 0x1c, 0xa5, 0xf8, 0x00, 0x1c, 0xa4, 0xf8, 0x00, 0x1b, 0xa3, 0xf9, 0x00, 0x1b, 0xa3, 0xf9, 0x00, 0x1b, 0xa2, + 0xf9, 0x00, 0x19, 0xa1, 0xf9, 0x00, 0x18, 0xa0, 0xf8, 0x00, 0x17, 0xa0, 0xf8, 0x00, 0x16, 0x9f, 0xf8, 0x00, 0x15, + 0x9e, 0xf7, 0x00, 0x14, 0x9d, 0xf7, 0x00, 0x13, 0x9c, 0xf6, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, 0x00, + 0x12, 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x55, 0xd0, 0xf9, 0x00, 0x53, 0xd0, 0xfa, 0x00, 0x51, 0xd0, 0xfa, + 0x00, 0x4f, 0xcf, 0xfa, 0x00, 0x4d, 0xcf, 0xfa, 0x00, 0x4b, 0xce, 0xfa, 0x00, 0x49, 0xcd, 0xf9, 0x00, 0x46, 0xcc, + 0xf9, 0x00, 0x44, 0xca, 0xf8, 0x00, 0x43, 0xca, 0xf8, 0x00, 0x43, 0xc9, 0xf8, 0x00, 0x43, 0xc8, 0xf9, 0x00, 0x42, + 0xc8, 0xf9, 0x00, 0x42, 0xc7, 0xf9, 0x00, 0x41, 0xc6, 0xf9, 0x00, 0x41, 0xc6, 0xf9, 0x00, 0x40, 0xc5, 0xfa, 0x00, + 0x3e, 0xc3, 0xf9, 0x00, 0x3d, 0xc2, 0xfa, 0x00, 0x3b, 0xc1, 0xfa, 0x00, 0x39, 0xc0, 0xfa, 0x00, 0x38, 0xbf, 0xf9, + 0x00, 0x36, 0xbf, 0xf9, 0x00, 0x35, 0xbe, 0xf9, 0x00, 0x34, 0xbd, 0xf8, 0x00, 0x33, 0xbc, 0xf9, 0x00, 0x33, 0xba, + 0xfa, 0x00, 0x32, 0xb9, 0xfb, 0x00, 0x32, 0xb8, 0xfc, 0x00, 0x30, 0xb7, 0xfa, 0x00, 0x2e, 0xb6, 0xf8, 0x00, 0x2d, + 0xb5, 0xf7, 0x00, 0x2b, 0xb4, 0xf5, 0x00, 0x2b, 0xb4, 0xf6, 0x00, 0x2b, 0xb3, 0xf7, 0x00, 0x2b, 0xb2, 0xf8, 0x00, + 0x2b, 0xb1, 0xf8, 0x00, 0x22, 0xaf, 0xf9, 0x00, 0x19, 0xac, 0xfa, 0x00, 0x1e, 0xad, 0xf7, 0x00, 0x24, 0xae, 0xf3, + 0x00, 0x20, 0xad, 0xf5, 0x00, 0x1d, 0xab, 0xf6, 0x00, 0x1f, 0xab, 0xf6, 0x00, 0x20, 0xaa, 0xf5, 0x00, 0x1f, 0xa9, + 0xf6, 0x00, 0x1e, 0xa8, 0xf7, 0x00, 0x1d, 0xa6, 0xf7, 0x00, 0x1c, 0xa5, 0xf8, 0x00, 0x1c, 0xa4, 0xf8, 0x00, 0x1b, + 0xa3, 0xf9, 0x00, 0x1b, 0xa3, 0xf9, 0x00, 0x1b, 0xa2, 0xf9, 0x00, 0x19, 0xa1, 0xf9, 0x00, 0x18, 0xa0, 0xf8, 0x00, + 0x17, 0xa0, 0xf8, 0x00, 0x16, 0x9f, 0xf8, 0x00, 0x15, 0x9e, 0xf7, 0x00, 0x14, 0x9d, 0xf7, 0x00, 0x13, 0x9c, 0xf6, + 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x55, 0xd0, + 0xf9, 0x00, 0x53, 0xd0, 0xfa, 0x00, 0x51, 0xd0, 0xfa, 0x00, 0x4f, 0xcf, 0xfa, 0x00, 0x4d, 0xcf, 0xfa, 0x00, 0x4b, + 0xce, 0xfa, 0x00, 0x49, 0xcd, 0xf9, 0x00, 0x46, 0xcc, 0xf9, 0x00, 0x44, 0xca, 0xf8, 0x00, 0x43, 0xca, 0xf8, 0x00, + 0x43, 0xc9, 0xf8, 0x00, 0x43, 0xc8, 0xf9, 0x00, 0x42, 0xc8, 0xf9, 0x00, 0x42, 0xc7, 0xf9, 0x00, 0x41, 0xc6, 0xf9, + 0x00, 0x41, 0xc6, 0xf9, 0x00, 0x40, 0xc5, 0xfa, 0x00, 0x3e, 0xc3, 0xf9, 0x00, 0x3d, 0xc2, 0xfa, 0x00, 0x3b, 0xc1, + 0xfa, 0x00, 0x39, 0xc0, 0xfa, 0x00, 0x38, 0xbf, 0xf9, 0x00, 0x36, 0xbf, 0xf9, 0x00, 0x35, 0xbe, 0xf9, 0x00, 0x34, + 0xbd, 0xf8, 0x00, 0x33, 0xbc, 0xf9, 0x00, 0x33, 0xba, 0xfa, 0x00, 0x32, 0xb9, 0xfb, 0x00, 0x32, 0xb8, 0xfc, 0x00, + 0x30, 0xb7, 0xfa, 0x00, 0x2e, 0xb6, 0xf8, 0x00, 0x2d, 0xb5, 0xf7, 0x00, 0x2b, 0xb4, 0xf5, 0x00, 0x2b, 0xb4, 0xf6, + 0x00, 0x2b, 0xb3, 0xf7, 0x00, 0x2b, 0xb2, 0xf8, 0x00, 0x2b, 0xb1, 0xf8, 0x00, 0x22, 0xaf, 0xf9, 0x00, 0x19, 0xac, + 0xfa, 0x00, 0x1e, 0xad, 0xf7, 0x00, 0x24, 0xae, 0xf3, 0x00, 0x20, 0xad, 0xf5, 0x00, 0x1d, 0xab, 0xf6, 0x00, 0x1f, + 0xab, 0xf6, 0x00, 0x20, 0xaa, 0xf5, 0x00, 0x1f, 0xa9, 0xf6, 0x00, 0x1e, 0xa8, 0xf7, 0x00, 0x1d, 0xa6, 0xf7, 0x00, + 0x1c, 0xa5, 0xf8, 0x00, 0x1c, 0xa4, 0xf8, 0x00, 0x1b, 0xa3, 0xf9, 0x00, 0x1b, 0xa3, 0xf9, 0x00, 0x1b, 0xa2, 0xf9, + 0x00, 0x19, 0xa1, 0xf9, 0x00, 0x18, 0xa0, 0xf8, 0x00, 0x17, 0xa0, 0xf8, 0x00, 0x16, 0x9f, 0xf8, 0x00, 0x15, 0x9e, + 0xf7, 0x00, 0x14, 0x9d, 0xf7, 0x00, 0x13, 0x9c, 0xf6, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, 0x00, 0x12, + 0x9b, 0xf5, 0x00, 0x12, 0x9b, 0xf5, +]; diff --git a/crates/ironrdp-testsuite-core/tests/graphics/dwt.rs b/crates/ironrdp-testsuite-core/tests/graphics/dwt.rs new file mode 100644 index 00000000..d4f97a26 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/graphics/dwt.rs @@ -0,0 +1,851 @@ +use ironrdp_graphics::dwt::*; + +#[test] +fn encode_works_for_min_values() { + let mut buffer = [-32768; 4096]; + let expected = ENCODED_DWT_FOR_MIN_VALUES; + + let mut temp = vec![0; 4096]; + encode(&mut buffer, temp.as_mut_slice()); + assert_eq!(expected.as_ref(), buffer.as_ref()); +} + +#[test] +fn encode_works_for_max_values() { + let mut buffer = [32767; 4096]; + let expected = ENCODED_DWT_FOR_MAX_VALUES; + + let mut temp = vec![0; 4096]; + encode(&mut buffer, temp.as_mut_slice()); + assert_eq!(expected.as_ref(), buffer.as_ref()); +} + +#[test] +fn encode_works_for_regular_values() { + let mut buffer = DECODED_DWT; + let expected = ENCODED_DWT; + + let mut temp = vec![0; 4096]; + encode(&mut buffer, temp.as_mut_slice()); + assert_eq!(expected.as_ref(), buffer.as_ref()); +} + +#[test] +fn decode_works_for_min_values() { + let mut buffer = [-32768; 4096]; + let expected = [0; 4096]; + + let mut temp = vec![0; 4096]; + decode(&mut buffer, temp.as_mut_slice()); + assert_eq!(expected.as_ref(), buffer.as_ref()); +} + +#[test] +fn decode_works_for_max_values() { + let mut buffer = [32767; 4096]; + let expected = DECODED_DWT_FOR_MAX_VALUES; + + let mut temp = vec![0; 4096]; + decode(&mut buffer, temp.as_mut_slice()); + assert_eq!(expected.as_ref(), buffer.as_ref()); +} + +#[test] +fn decode_works_for_regular_values() { + let mut buffer = ENCODED_DWT; + let expected = DECODED_DWT; + + let mut temp = vec![0; 4096]; + decode(&mut buffer, temp.as_mut_slice()); + assert_eq!(expected.as_ref(), buffer.as_ref()); +} + +// Is this actually correct? +const ENCODED_DWT_FOR_MIN_VALUES: [i16; 4096] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, + -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, + -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, + -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, + -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, -32768, +]; + +// Is this actually correct? +const ENCODED_DWT_FOR_MAX_VALUES: [i16; 4096] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, +]; + +const DECODED_DWT_FOR_MAX_VALUES: [i16; 4096] = [ + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4092, 8191, -4100, -16383, -4100, + 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, + -4, 16383, 16379, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, + 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, 32764, 32764, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -2, -2, -2, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, + -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, + 32764, 32764, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, + -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, 16383, 16379, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, + 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, 16383, 16379, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, + 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -2, 32764, 32764, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -6, 16381, 16377, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -2, + -2, -2, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, + -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, 32764, 32764, 2, 4092, 8191, -4100, -16383, -4100, 8191, + 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, + 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, + -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, + 16383, 16379, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4092, 8191, -4100, + -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, + 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4, 16383, 16379, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, + 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, 32764, 32764, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, -16383, -32768, -16385, + -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -2, -2, -2, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, + -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, + 32764, 32764, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, + -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, 16383, 16379, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, + 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, 16383, 16379, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, + 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -2, 32764, 32764, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -6, 16381, 16377, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -2, + -2, -2, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, + -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, 32764, 32764, 2, 4092, 8191, -4100, -16383, -4100, 8191, + 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, + 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, + -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, + 16383, 16379, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4092, 8191, -4100, + -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, + 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4, 16383, 16379, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, + 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, 32764, 32764, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, -16383, -32768, -16385, + -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -2, -2, -2, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, + -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, + 32764, 32764, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, + -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, 16383, 16379, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, + 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, 16383, 16379, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, + 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -2, 32764, 32764, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -6, 16381, 16377, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -2, + -2, -2, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, + -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, 32764, 32764, 2, 4092, 8191, -4100, -16383, -4100, 8191, + 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, + 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, + -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, + 16383, 16379, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4092, 8191, -4100, + -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, + 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4, 16383, 16379, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, + 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, 32764, 32764, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, + -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, -16383, -32768, -16385, + -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -2, -2, -2, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, + -4100, 2, -4100, -8194, -12294, -16385, -6, 16381, 16377, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, + -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -2, + 32764, 32764, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, + -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, 16383, 16379, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, + 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, + 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, + -4100, -16383, -4100, 8191, 4092, 2, 4092, 8191, -4100, -16383, -4, 16383, 16379, 2, 8191, 16381, -8194, -32768, + -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, + 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, + 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, -8194, -32768, -8194, 16381, 8191, 2, 8191, 16381, + -8194, -32768, -2, 32764, 32764, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, -16385, -12294, -8194, -4100, 2, -4100, -8194, -12294, + -16385, -6, 16381, 16377, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, + -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -16385, -32768, -16383, 2, -16383, -32768, -16385, -2, -2, + -2, -2, 2, -4, -2, -6, -2, -6, -2, -4, 2, -4, -2, -6, -2, -6, -2, -4, 2, -4, -2, -6, -2, -6, -2, -4, 2, -4, -2, -6, + -2, -6, -2, -4, 2, -4, -2, -6, -2, -6, -2, -4, 2, -4, -2, -6, -2, -6, -2, -4, 2, -4, -2, -6, -2, -6, -2, -4, 2, -4, + -2, -6, -2, -7, -4, -8, 2, 16383, 32764, 16381, -2, 16381, 32764, 16383, 2, 16383, 32764, 16381, -2, 16381, 32764, + 16383, 2, 16383, 32764, 16381, -2, 16381, 32764, 16383, 2, 16383, 32764, 16381, -2, 16381, 32764, 16383, 2, 16383, + 32764, 16381, -2, 16381, 32764, 16383, 2, 16383, 32764, 16381, -2, 16381, 32764, 16383, 2, 16383, 32764, 16381, -2, + 16381, 32764, 16383, 2, 16383, 32764, 16381, -2, -4, -6, -6, 2, 16379, 32764, 16377, -2, 16377, 32764, 16379, 2, + 16379, 32764, 16377, -2, 16377, 32764, 16379, 2, 16379, 32764, 16377, -2, 16377, 32764, 16379, 2, 16379, 32764, + 16377, -2, 16377, 32764, 16379, 2, 16379, 32764, 16377, -2, 16377, 32764, 16379, 2, 16379, 32764, 16377, -2, 16377, + 32764, 16379, 2, 16379, 32764, 16377, -2, 16377, 32764, 16379, 2, 16379, 32764, 16377, -2, -8, -6, -10, +]; + +const ENCODED_DWT: [i16; 4096] = [ + 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 256, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -128, 128, 0, 0, 0, 0, 0, 0, 0, 0, -384, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 128, 0, + -256, 512, -640, -128, 0, 0, 0, 0, 0, 0, -128, -384, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -384, 0, 384, 0, 0, 0, 0, 0, 0, + -128, 512, 512, -640, -128, 0, 0, 0, 0, 0, 0, -256, 640, 0, 0, 0, 0, 0, 0, 256, 128, 0, 256, -640, -512, 128, 0, 0, + 0, 0, 0, 0, 128, 384, -128, 640, 0, -128, 0, 0, 0, 0, 0, -256, -256, -384, 0, 0, 0, 0, 0, 384, 0, 0, 896, 0, -512, + 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, -128, 256, -128, 0, 0, 0, 0, 0, -384, 128, 640, 0, 0, 0, 0, 0, 0, -128, 0, 0, 128, + 896, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, -256, 0, 0, 0, 0, 0, 128, 0, -1152, -384, + 640, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, -128, -128, -128, 0, 0, 0, 0, 0, 0, 768, -256, + -1024, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, -384, 0, 0, 0, 0, 0, 0, -128, 0, -128, 0, 640, + 256, 384, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 256, 0, 0, 0, 0, 0, 0, -128, 128, 0, 0, -128, 256, 0, 0, 128, 0, 0, -128, + 0, -128, 896, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -384, -256, 0, 0, 0, 0, 0, 384, -128, 0, -128, 0, 128, 0, 0, 0, 128, + 256, -128, 0, -640, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, -384, -512, 0, 0, 0, 0, 0, -128, -768, 128, 0, -128, 0, 0, 0, + 0, 0, 0, 0, 768, 0, -128, 0, 128, 0, 0, 128, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 384, -640, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -128, -128, 128, 1280, 0, 0, 0, -128, 128, 0, 0, 0, 0, -128, 128, 0, 0, 0, 0, 0, 0, 896, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 128, -640, -640, 384, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, -128, 0, 0, 0, 0, 0, -128, -256, 0, 0, 0, -128, 0, + 0, 0, 0, 0, 0, 384, -128, -896, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, -768, 0, 0, 0, 0, 0, 0, -1024, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 384, 256, 512, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 768, -128, 0, 0, 0, 0, -128, 896, -128, 0, 0, 0, -128, + -128, 0, 0, 0, 0, 0, -384, -128, 768, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 384, 128, 0, 0, 0, 0, 0, 512, 128, 256, -256, + -256, 0, -128, 0, 0, 0, 0, 128, -128, -896, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -768, 0, 0, 0, 0, 0, -128, -1280, + 384, 128, 256, 0, -256, 128, 128, 0, 0, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 384, -256, 0, 0, 0, 0, 0, + 768, -1024, -256, 0, -256, 128, 0, 0, -128, -128, -128, 128, 128, 128, 768, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 512, 128, + 0, 0, 0, 0, 0, 384, 256, 0, 128, 0, 0, 128, 128, 0, 0, 128, -256, 640, -384, 256, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, + -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, -384, 0, 128, 0, 0, 128, 128, 0, 0, 0, 0, 0, + -512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, -128, -128, 0, 0, 128, 0, 0, 0, 384, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 128, 128, 128, -256, 0, 128, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, -256, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 384, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, -128, -128, 0, 0, -128, -128, + 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, -128, -128, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, -128, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, 0, 0, + 0, 0, 0, 0, -256, -1024, 256, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, + 128, 0, -256, 512, 512, -128, 0, 0, 0, 0, 0, 0, -128, 384, -128, 0, 0, 0, 0, 0, 0, 128, 0, 0, 128, 0, 0, 0, 0, 0, + 0, 0, 0, -128, 384, 384, -896, -128, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, 0, 0, 0, + 0, 0, 0, 0, 128, 0, 0, 768, 384, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -1024, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, -256, -256, 256, -128, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 128, 128, 384, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 0, -128, -640, -384, -512, -768, -768, 896, 128, 128, -128, 0, 0, 0, 0, 0, 0, 0, + -128, -256, -384, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 256, -128, 0, -256, -384, 128, 128, -1024, 768, 0, -128, + 128, 0, 0, 0, 0, 128, 128, 0, 0, 768, -384, -128, 0, 0, 0, 0, 0, -128, 640, 256, -768, -256, 0, 0, 0, 256, 896, + -768, -128, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, -128, -128, 128, 768, 0, 0, 0, 0, 0, 0, -256, 128, 0, 0, 0, 0, 0, 0, + 0, -128, 768, -128, 128, -768, 896, 0, 0, 0, 0, 0, 0, 0, 0, 128, -128, -128, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -128, -256, 0, -1024, 768, 768, 512, 384, 384, 512, -128, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 256, -128, -128, 896, -1024, 0, 128, -256, -512, -384, -128, 128, -384, -512, 128, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 128, -384, -640, 0, 128, 0, 0, 1024, -128, -896, -1152, -896, -256, 768, 384, 0, + 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, -128, -128, 0, 0, 0, 0, 0, 128, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, 0, 0, 0, 0, 0, -128, 128, 0, 0, 128, -128, 0, 0, 0, + 0, -128, 128, -128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 128, -128, 0, 0, 0, 0, + 0, 0, 128, 128, 384, 384, 0, 128, 128, -128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, + 128, 0, 0, 0, 256, 128, -512, -640, -1024, -768, -512, 128, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 128, 0, 0, 0, 0, 384, -1152, -640, 1280, 256, 256, 0, -128, 256, 768, -512, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 256, -128, -128, 128, 0, 0, 0, 0, 0, -128, 512, -256, -128, 128, -128, 128, -128, + 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, -128, 128, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, -128, 0, -128, 128, 128, 0, 128, + 0, -128, 384, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, -128, -128, 128, 128, 128, + 0, -256, -384, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, 512, 256, 128, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 256, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -512, -256, 256, 0, 0, 0, 0, + 0, 0, 0, -256, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, -256, 256, -512, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 256, -256, 0, 0, 0, 0, 0, + -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 256, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 256, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, + 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, + 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -256, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 256, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, -256, 512, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 256, -256, 0, 0, 0, 0, 0, 0, 0, 256, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, + -256, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 64, 0, 0, 64, 0, 0, 0, 0, 0, 0, -64, 0, 0, -64, + -64, 0, -320, 192, -128, 64, 0, -64, 0, 0, 0, 0, 64, -256, -64, 0, 0, 64, -320, -128, 64, 64, 64, 384, -128, -64, + 0, 64, -256, 1344, 384, 0, 0, -320, -64, 64, 128, 256, 256, 192, 320, 0, 0, -64, 256, -512, -704, 0, 0, -128, 512, + -64, 0, 192, 64, -1152, 1024, 64, -64, 128, 0, -256, -256, 0, 0, -384, 64, -192, -64, 64, 896, -1472, 64, 0, 64, 0, + 64, -192, -64, 0, 0, 0, -320, -128, 192, -64, 704, -512, -192, 0, 64, 0, -64, -192, 64, 0, 896, 64, 0, -64, 0, 0, + -960, -128, 448, 64, -64, 64, -192, -64, -64, -128, -896, 0, 64, 0, 0, 320, -1152, 896, -448, -64, -64, -128, 832, + 64, -64, 192, -896, 64, -128, 128, -64, 704, -1024, -128, 128, -64, 64, 0, -576, 0, -64, 1216, 64, 64, 64, 0, 128, + 128, 512, 0, 0, 0, 0, 64, -832, 0, 0, -384, -192, -64, 0, -128, -128, 384, 832, 0, 0, 0, 64, 576, 64, 0, 0, -64, + 128, 0, 0, 64, 0, -320, 0, 0, -64, 0, 0, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 64, -128, -256, 64, 256, 256, -128, 0, 0, + -64, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, -256, -64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -128, + -64, 64, 64, -64, -192, 0, -128, -192, 0, 0, 0, 0, 0, 0, 0, -128, -128, 0, 64, -64, 128, 0, 0, 0, 64, -64, 64, 0, + 64, -512, -64, 0, 0, 64, -192, 0, 0, -64, 192, 0, 0, 64, -192, -192, -192, 192, -256, 0, 0, 0, 64, 0, 0, 192, -192, + 192, -64, 64, 64, 0, 64, -320, 1280, 0, 0, 0, -64, 128, 448, -64, 0, 192, -128, 64, 0, 256, -64, 0, 64, 0, 64, + -192, 128, -576, -768, -1216, 640, -64, 128, 0, -64, 64, 576, -832, 0, 0, 64, -256, -832, 0, 0, 1024, -448, 0, + 1024, -192, 64, -128, -64, 128, -64, 0, 0, 0, 64, 64, 0, 0, 256, -192, -1088, -896, 128, -256, -896, -64, 0, -64, + 64, 0, -64, 0, 64, 64, -128, 64, 192, 1088, 832, 960, 576, -128, 0, 128, -192, 128, 128, -128, 128, 64, -64, 64, 0, + 0, -128, 320, 448, -64, 0, 64, 256, -448, -896, -448, 128, -128, 0, 0, 0, 0, 0, 0, -64, 64, 0, -192, 1088, 512, + -64, 256, 512, -576, 384, 0, 64, 0, 0, 0, 0, 0, 0, -64, 64, 0, 0, 0, 0, 0, -128, -128, 128, 0, 0, 320, -64, 0, 0, + -64, 0, 0, 0, 0, 0, 0, 0, -320, -256, -128, -384, -64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -448, 64, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -128, 0, -512, -128, 0, 0, 0, 0, 0, 0, -128, -128, 128, -128, 0, 0, 0, 0, 256, 256, 0, 0, 0, 384, + 0, 0, 128, 128, -128, 128, 0, 0, 0, 0, -512, -1024, 0, 0, 0, -128, 0, 0, -128, -128, -128, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 128, 128, -128, -384, 384, 0, 0, 0, -128, 128, 0, 0, 0, 0, 128, -256, 0, 0, -128, 384, 128, 256, -128, 0, 0, + 0, -256, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 128, 128, 0, -384, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, -128, 256, 0, + -128, 128, 0, 0, 0, 128, 128, 0, 0, 0, 0, 0, -128, 0, 0, 0, -128, 0, -256, 0, 0, 256, 256, 128, -128, 0, -128, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -256, 256, 0, 0, 128, -256, 128, -128, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, + 0, 0, 0, 0, -128, -256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 128, -128, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, -64, -64, 32, 0, -32, -64, 0, -32, 256, 192, 96, 0, -32, -192, 0, 128, 160, -416, + -1696, -96, -64, 0, -64, -480, -224, 800, 256, 96, 224, -800, 832, -384, 160, 640, 192, 224, -160, -416, 64, -64, + 160, -1728, 320, -64, 960, 96, -1472, 352, -288, -32, -64, 32, -576, -64, 256, -96, 96, -160, -128, -32, -160, -64, + -96, 0, 32, -320, -96, 32, -32, 32, -160, -32, -96, 96, 224, -448, -416, 320, 0, -32, 352, 64, 160, 64, 384, 0, + -256, 768, -672, -1568, -128, 320, 288, 992, -32, 160, 320, 32, 416, -1824, -1664, 288, 384, -192, -576, 160, 32, + -96, -128, -32, 416, 672, 256, 352, 96, 224, 224, -64, -96, -32, 0, 64, -448, -608, -224, 32, -32, -32, -96, -64, + 0, 0, -32, -32, -32, -224, 96, 128, 224, -32, -192, -448, -32, -224, 96, -384, -512, -64, 64, -32, -288, 32, 224, + -672, 800, -128, -64, -736, 64, 64, 96, -128, 704, -128, 160, -128, -64, -64, 128, -224, 0, 0, 96, 64, 544, -192, + 224, 160, 96, 32, -160, 0, 32, 0, 0, 192, 32, -128, 64, 0, -32, 0, -32, -480, -384, -384, -608, -736, 864, 448, 96, + 1184, 352, -384, -480, -512, 928, 800, 1472, 3296, 2752, 1376, 832, 352, 1216, 992, 1792, 2752, 2016, 4096, 3680, + 864, 2304, -992, 1728, 4000, 288, 3104, 2144, 320, 2144, -1088, 1920, 2720, 3264, 3424, 3616, 192, 672, -320, 320, + -512, 4032, 3456, 2720, -320, 1888, 1664, 1248, 960, 1120, 1696, 480, -96, +]; +const DECODED_DWT: [i16; 4096] = [ + -32, 16, 64, 272, -32, -16, 0, -16, -32, -24, -16, -8, 0, -24, -48, -72, -96, -90, -84, -78, -72, -98, -124, -150, + -176, -192, -208, -224, -240, -256, -272, -288, -304, -304, -304, -304, -304, -336, -368, -400, -432, -450, -468, + -486, -504, -522, -540, -558, -576, -598, -620, -642, -664, -686, -708, -730, -752, -768, -784, -800, -816, -816, + -816, -816, 68, 120, 172, 240, 53, 55, 57, 43, 30, 32, 34, 36, 38, 20, 2, -16, -34, -36, -38, -40, -42, -68, -94, + -120, -146, -149, -152, -186, -221, -228, -234, -241, -247, -255, -262, -269, -276, -303, -330, -357, -384, -404, + -424, -444, -463, -485, -507, -529, -550, -573, -595, -617, -639, -674, -708, -710, -712, -733, -754, -775, -796, + -796, -796, -796, 168, 224, 281, 209, 138, 126, 115, 103, 92, 88, 84, 80, 76, 64, 52, 40, 28, 18, 8, -2, -12, -38, + -64, -90, -116, -106, -95, -148, -201, -199, -196, -193, -190, -205, -219, -233, -247, -270, -292, -314, -336, + -358, -379, -401, -422, -448, -473, -499, -524, -547, -569, -592, -614, -661, -707, -690, -672, -698, -724, -750, + -776, -776, -776, -776, 268, 312, 357, 274, 191, 181, 172, 163, 154, 144, 134, 124, 114, 108, 102, 80, 58, 56, 54, + 52, 50, 24, -2, -44, -86, -63, -38, -94, -150, -138, -126, -146, -165, -171, -176, -198, -219, -237, -254, -271, + -288, -312, -335, -358, -381, -411, -440, -469, -498, -521, -544, -567, -589, -648, -707, -670, -632, -663, -694, + -725, -756, -756, -756, -756, 368, 401, 434, 339, 244, 237, 230, 223, 216, 200, 184, 168, 152, 152, 152, 120, 88, + 94, 100, 106, 112, 86, 60, 2, -56, -19, 19, -40, -98, -77, -55, -98, -140, -137, -133, -162, -190, -203, -215, + -228, -240, -265, -290, -315, -340, -373, -406, -439, -472, -495, -518, -541, -564, -635, -706, -649, -592, -628, + -664, -700, -736, -736, -736, -736, 404, 557, 454, 383, 313, 532, 239, 282, 326, 304, 282, 260, 238, 246, 254, 118, + 238, 196, 154, 32, -90, -88, -86, 76, 238, 242, 247, 28, -191, -232, -273, -123, 29, -63, -155, -151, -146, -164, + -181, -199, -216, -241, -266, -291, -315, -346, -377, -408, -438, -448, -457, -498, -539, -597, -654, -503, -608, + -625, -642, -675, -708, -708, -708, -708, 440, 713, 475, 428, 382, 827, 249, 342, 436, 408, 380, 352, 324, 340, + 356, -140, -124, 42, 208, 214, 220, 250, 280, 406, 532, 504, 476, 352, 229, 125, 21, -147, -314, -245, -176, -139, + -101, -124, -147, -170, -192, -217, -241, -266, -290, -319, -347, -376, -404, -400, -395, -455, -514, -558, -601, + -357, -624, -622, -620, -650, -680, -680, -680, -680, 604, 677, 495, 457, 419, 770, 354, 386, 418, 416, 414, 380, + 346, 258, -342, -302, -6, 288, 582, 604, 626, 588, 550, 688, 826, 829, 833, 724, 616, 482, 347, 181, 15, -139, + -293, -175, -57, -85, -113, -141, -168, -193, -217, -241, -265, -292, -318, -344, -370, -352, -334, -412, -489, + -487, -485, -403, -576, -587, -598, -625, -652, -652, -652, -652, 1280, 1154, 1028, 998, 968, 970, 460, 430, 400, + 424, 448, 408, 368, 432, -528, -208, 112, 534, 956, 994, 1032, 926, 820, 970, 1120, 1155, 1190, 1097, 1004, 839, + 674, 509, 344, 223, 102, 45, -12, -45, -78, -111, -144, -168, -192, -216, -240, -264, -288, -312, -336, -304, -272, + -368, -464, -416, -368, -448, -528, -552, -576, -600, -624, -624, -624, -624, 770, 671, 573, 554, 536, 629, 467, + 464, 462, 492, 523, 490, 457, 281, -406, -101, 204, 599, 995, 1310, 1370, 1297, 1225, 1296, 1368, 1433, 1498, 1403, + 1308, 1185, 1062, 875, 688, 586, 485, 304, 123, -83, -32, -77, -122, -175, -227, -200, -172, -194, -217, -239, + -261, -315, -368, -326, -283, -361, -438, -452, -465, -515, -565, -583, -601, -617, -633, -633, 772, 701, 630, 623, + 616, 545, 474, 499, 524, 561, 599, 572, 546, 131, -283, 6, 296, 665, 1034, 1627, 1708, 1669, 1630, 1623, 1616, + 1711, 1806, 1709, 1612, 1531, 1450, 1241, 1032, 950, 869, 563, 258, -120, 15, -43, -100, -181, -262, -183, -103, + -124, -145, -166, -186, -325, -464, -283, -102, -305, -508, -455, -402, -478, -554, -566, -578, -610, -642, -642, + 774, 730, 687, 675, 664, 620, 577, 581, 586, 598, 610, 590, 571, -147, -97, 209, 516, 794, 1073, 1575, 1822, 1976, + 1875, 1869, 1864, 1989, 2114, 2015, 1916, 1877, 1838, 1607, 1376, 1266, 1156, 902, 137, -61, -3, -121, -238, -124, + -9, -70, -131, -166, -201, -221, -239, -272, -304, -129, -209, -298, -386, -427, -467, -937, -895, -549, -459, + -667, -619, -619, 776, 760, 744, 728, 712, 696, 680, 664, 648, 635, 622, 609, 596, -425, 90, 413, 736, 924, 1112, + 1524, 1936, 2284, 2120, 2116, 2112, 2267, 2422, 2321, 2220, 2223, 2226, 1973, 1720, 1582, 1444, 1242, 16, -2, -20, + 58, 136, -66, -267, -213, -158, -208, -257, -275, -292, -218, -144, 26, -316, -290, -264, -142, -20, 2956, 2860, + -788, -852, -980, -596, -596, 826, 807, 789, 770, 752, 749, 747, 744, 742, 677, 613, 517, 421, -286, 288, 574, 860, + 1081, 1303, 1668, 2034, 2313, 2337, 2344, 2352, 2453, 2554, 2575, 2596, 2507, 2418, 2249, 2080, 1961, 1843, 925, 7, + 40, 74, 748, 654, 451, 250, 48, -154, -108, -62, -112, -161, -29, 104, 44, -271, -275, -278, -842, 1411, 3007, + 3323, 327, -1389, -1197, -493, -493, 876, 855, 834, 813, 792, 803, 814, 825, 836, 720, 605, 681, 758, 110, 487, + 735, 984, 1239, 1494, 1813, 2132, 2343, 2554, 2573, 2592, 2639, 2686, 2829, 2972, 2791, 2610, 2525, 2440, 2341, + 2243, 608, -2, 83, 169, 1438, 1172, 969, 767, 565, 363, 248, 134, 52, -30, -95, -160, -193, -226, -259, -292, 763, + -742, 2290, 1738, -1118, -902, -902, -390, -390, 926, 902, 879, 855, 832, 824, 817, 809, 802, 763, 724, 397, 2375, + 970, 589, 848, 1108, 1396, 1685, 1941, 2198, 2468, 2739, 2785, 2832, 2889, 2946, 3179, 2900, 3059, 2962, 2849, + 2736, 2897, 2546, -365, 309, 206, 871, 1760, 1626, 1471, 1316, 1146, 975, 844, 714, 599, 485, 350, 216, 145, 75, + -356, 750, 2687, 529, -1067, -615, -835, -799, -847, -383, -383, 976, 950, 924, 898, 872, 846, 820, 794, 768, 806, + 844, 882, 1432, 2598, 692, 962, 1232, 1554, 1876, 2070, 2264, 2594, 2924, 2998, 3072, 3139, 3206, 3273, 2316, 3071, + 3314, 3173, 3032, 2941, 1826, -57, 108, 73, 1574, 2083, 2080, 1973, 1866, 1727, 1588, 1441, 1294, 1147, 1000, 796, + 592, 484, 376, 828, 256, 772, -248, -72, -408, 984, -184, -536, -376, -376, 1026, 997, 969, 941, 913, 888, 864, + 840, 816, 762, 709, 768, 1339, 2269, 2176, 1411, 1414, 1677, 1941, 2188, 2436, 2730, 3023, 3157, 3291, 3350, 3409, + 3420, 2152, 3001, 3594, 3403, 3213, 3234, 951, 12, 97, -302, 2883, 2756, 2373, 2312, 2252, 2144, 2036, 1861, 1687, + 1545, 1403, 1254, 1106, 974, 842, 1229, 1105, 21, 217, 46, -381, 1912, 3181, 2765, 301, -723, 1076, 1045, 1015, + 984, 954, 931, 909, 886, 864, 719, 575, 654, 1246, 1685, 3149, 1604, 1596, 1801, 2006, 2307, 2609, 2866, 3123, + 3316, 3510, 3561, 3613, 3568, 1988, 2931, 3875, 3634, 3394, 3527, 76, 81, 86, 859, 3168, 2917, 2666, 2652, 2639, + 2561, 2484, 2282, 2081, 1943, 1806, 1713, 1621, 1464, 1308, 1119, 931, 550, 170, -92, -354, 1560, 3986, 1970, -558, + -558, 1126, 1093, 1060, 1027, 995, 974, 953, 932, 912, 900, 888, -340, 1249, 1757, 2521, 2421, 1810, 2036, 2263, + 2522, 2781, 3066, 3351, 3443, 3537, 3612, 3688, 3476, 2496, 3021, 3803, 3833, 3863, 2844, 33, 134, -21, 2100, 3197, + 3062, 2927, 2944, 2961, 2882, 2804, 2607, 2410, 2309, 2209, 2140, 2071, 1842, 1614, 1329, 1044, 663, 283, 10, -263, + -488, -201, -201, -457, -457, 1176, 1141, 1106, 1071, 1036, 1017, 998, 979, 960, 825, 690, 203, 740, 1573, 1894, + 3239, 2024, 2272, 2521, 2737, 2954, 3010, 3067, 3315, 3564, 3664, 3764, 3384, 3004, 3112, 3732, 3776, 3820, 1905, + -10, 187, -128, 3341, 3226, 3207, 3188, 3236, 3284, 3204, 3124, 2932, 2740, 2676, 2612, 2567, 2522, 2221, 1920, + 1539, 1158, 777, 396, 112, -172, -488, -292, -324, -356, -356, 1194, 1162, 1131, 1100, 1069, 1047, 1026, 973, 920, + 969, 507, 381, 767, 1428, 1834, 2800, 2486, 2347, 2722, 2920, 3118, 3290, 3462, 3266, 3071, 3157, 3243, 3521, 3800, + 3674, 3548, 3710, 3873, 874, 179, 92, 517, 3440, 3291, 3334, 3377, 3403, 3430, 3361, 3292, 3174, 3057, 3004, 2951, + 2761, 2572, 2223, 1874, 1554, 1235, 884, 533, 220, -93, -470, -335, -319, -303, -303, 1212, 1184, 1157, 1129, 1102, + 1078, 1055, 967, 880, 1114, 325, 559, 794, 1284, 1775, 2361, 2948, 2423, 2923, 3103, 3283, 3314, 3346, 3474, 3602, + 3674, 3747, 3659, 3572, 3980, 3877, 3901, 3926, -157, 368, 253, 1674, 3795, 3356, 3461, 3566, 3571, 3577, 3518, + 3460, 3417, 3375, 3332, 3290, 2956, 2623, 2225, 1828, 1570, 1313, 991, 670, 328, -14, -452, -378, -314, -250, -250, + 1230, 1206, 1182, 1158, 1135, 1109, 1083, 1025, 968, 779, 78, 481, 885, 1284, 1939, 2466, 3250, 2627, 2772, 3158, + 3543, 3514, 3486, 3729, 3717, 3775, 3834, 3781, 3728, 3934, 3885, 3916, 2667, 92, 333, 174, 2831, 3702, 3549, 3588, + 3627, 3643, 3659, 3643, 3628, 3676, 3724, 3436, 3149, 2847, 2545, 2275, 2006, 1730, 1454, 1114, 775, 388, 1, -402, + -293, -309, -325, -325, 1248, 1228, 1208, 1188, 1168, 1140, 1112, 1084, 1056, 700, 344, 660, 976, 1284, 2104, 2316, + 3040, 2319, 2110, 2189, 2268, 2691, 3114, 3729, 3832, 3877, 3922, 3903, 3884, 3889, 3894, 3931, 1408, 341, 298, 95, + 3988, 3609, 3742, 3715, 3688, 3715, 3742, 3769, 3796, 3679, 3562, 3285, 3008, 2738, 2468, 2326, 2184, 1890, 1596, + 1238, 880, 448, 16, -352, -208, -304, -400, -400, 1296, 1284, 1272, 1260, 1249, 1165, 1081, 1093, 1106, 232, 382, + 677, 971, 973, 1232, 834, 693, 538, 639, 565, 490, 563, 637, -106, 944, 2358, 3773, 3795, 4074, 3964, 3855, 4337, + 212, 204, 197, 1342, 4023, 3813, 3860, 3811, 3762, 3766, 3771, 3776, 3781, 3604, 3427, 3202, 2977, 2838, 2699, + 2400, 2101, 1982, 1607, 1280, 954, 545, -120, -321, -266, -314, -362, -362, 1344, 1340, 1337, 1333, 1330, 1190, + 1051, 1103, 1156, 20, 933, 950, 967, 919, 872, 889, 906, 805, 705, 733, 761, 740, 720, 668, 616, 328, 40, 1640, + 3752, 3784, 3816, 3208, 40, 580, 97, 2589, 4058, 4018, 3979, 3907, 3836, 3818, 3801, 3784, 3767, 3529, 3292, 3375, + 3458, 3706, 3954, 3754, 3555, 2843, 1619, 1067, 516, 386, -256, -290, -324, -324, -324, -324, 1392, 1364, 1337, + 1310, 1283, 1247, 1212, 969, 982, 1424, 1100, 1079, 1058, 1073, 1088, 815, 799, 1056, 803, 773, 743, 645, 547, 769, + 736, 649, 563, 332, 102, 1939, 4033, 1982, 444, 332, -36, 4076, 4093, 4047, 4001, 3955, 3910, 3870, 3831, 3791, + 3752, 3806, 3861, 3836, 3811, 3678, 3545, 3380, 3216, 3639, 3807, 2342, 1134, 1091, 24, -387, -286, -286, -286, + -286, 1440, 1389, 1338, 1287, 1236, 1305, 1374, 1091, 1320, 1037, 1267, 1208, 1150, 715, 281, 486, 1204, 1564, 901, + 1325, 1750, 1830, 1911, 1383, 344, 459, 574, 817, 548, 351, 666, 757, 336, 340, 856, 4028, 4128, 4076, 4024, 4004, + 3984, 3922, 3861, 3799, 3738, 3828, 3919, 3785, 3652, 3394, 3137, 3007, 2878, 2900, 2923, 3105, 3800, 1284, 1328, + 28, -248, -248, -248, -248, 1456, 1407, 1358, 1309, 1261, 1210, 1159, 1444, 1218, 1265, 33, -655, -1343, -977, + -355, 394, 1401, 1753, 1338, 1739, 2140, 2575, 3010, 3524, 3784, 2536, 1033, 265, 522, 440, 615, 629, 388, 403, + 2211, 4051, 4099, 4078, 4058, 3990, 3922, 3910, 3898, 3886, 3875, 3805, 3736, 3554, 3373, 3126, 2880, 2585, 2291, + 2026, 1762, 2650, 3026, 2303, 2092, 665, -250, -250, -250, -250, 1472, 1425, 1379, 1332, 1286, 1371, 1457, 1030, + -932, -1834, -1712, -1238, -763, -621, 33, 815, 1598, 1943, 1776, 2153, 2531, 2808, 3085, 3362, 3640, 4102, 4052, + 3042, 496, 530, 564, 502, 440, 211, 3055, 3818, 4070, 4081, 4093, 3976, 3860, 3898, 3936, 3974, 4013, 3783, 3553, + 3323, 3094, 2858, 2623, 2420, 2217, 1921, 1626, 915, 2764, 250, 296, 22, -252, -252, -252, -252, 1488, 1443, 1399, + 1371, 1343, 1308, 1530, -408, -1834, -1590, -1089, -813, -536, -281, 485, 1172, 1859, 2132, 2150, 2503, 2857, 3105, + 3352, 3536, 3720, 3875, 3775, 4298, 4054, 2123, 449, 502, 556, 547, 26, 2113, 3945, 4116, 4031, 3946, 3862, 3838, + 3814, 3982, 3894, 3488, 3338, 3140, 2943, 2622, 2302, 2030, 1758, 1496, 1234, 1260, 774, -347, -188, -189, -190, + -222, -254, -254, 1504, 1462, 1420, 1410, 1400, 1246, 1604, -1334, -1712, -1089, -978, -643, -308, 59, 938, 1529, + 2120, 2322, 2524, 2854, 3184, 3402, 3620, 3710, 3800, 3905, 4010, 4019, 4028, 3973, 334, 503, 672, 627, 582, 409, + 236, 2359, 3970, 3917, 3864, 3778, 3692, 3990, 3776, 3194, 3124, 2958, 2792, 2387, 1982, 1641, 1300, 1071, 842, 69, + -192, -176, -160, -144, -128, -192, -256, -256, 1546, 1496, 1447, 1430, 1413, 1627, 1330, -2103, -1184, -820, -712, + -396, -80, 406, 1148, 1714, 2280, 2486, 2692, 2995, 3297, 3467, 3638, 3712, 3787, 3916, 4045, 3918, 4047, 3098, + 357, 656, 699, 198, 466, 381, 297, 376, 200, 1815, 3431, 3568, 3961, 4114, 3755, 3310, 3121, 2804, 2487, 2209, + 1931, 1189, 447, 37, -117, -255, -136, -111, -86, -109, -132, -196, -260, -260, 1588, 1531, 1475, 1450, 1426, 1497, + 33, -1592, -1168, -807, -446, -149, 148, 753, 1358, 1899, 2440, 2650, 2861, 3136, 3411, 3533, 3656, 3715, 3774, + 3927, 4080, 3817, 4066, 2223, 380, 553, 214, 3610, 350, 354, 358, 442, 526, 226, -74, 286, 1158, 1678, 1686, 1634, + 1582, 1114, 646, 239, -168, -31, 107, -228, -51, -66, -80, -46, -12, -74, -136, -200, -264, -264, 1630, 1566, 1502, + 1470, 1439, 1591, -817, -1401, -960, -634, -308, -14, 280, 876, 1472, 1972, 2472, 2718, 2966, 3229, 3492, 3583, + 3674, 3701, 3729, 3794, 3859, 4148, 4181, 708, 563, 418, 1297, 3917, 4234, 2198, 163, 267, 372, 348, 325, 108, 147, + 186, -31, 38, 107, 96, 85, 61, 37, -163, -106, -126, 111, 875, -152, -93, -34, -87, -140, -204, -268, -268, 1672, + 1601, 1530, 1491, 1452, 1685, -1666, -1209, -752, -461, -170, 121, 412, 999, 1586, 2045, 2504, 2787, 3071, 3322, + 3574, 3633, 3693, 3688, 3684, 3661, 3638, 3711, 2760, 473, 746, 283, 2380, 4225, 4022, 4043, 4064, 2141, 218, 215, + 212, 186, 160, 230, 300, 234, 168, 102, 36, -117, -269, 218, 1218, 2025, 2833, 1048, -224, -140, -56, -100, -144, + -208, -272, -272, 1626, 1607, 1589, 1459, 1585, 692, -1480, -1108, -736, -452, -168, 116, 400, 806, 1468, 1938, + 2408, 2703, 2999, 3327, 3655, 3569, 3483, 3620, 3759, 3440, 3121, 1602, 851, 820, 533, 438, 3415, 4252, 4066, 4055, + 4045, 4084, 4124, 2995, 1867, 1068, 269, 62, -145, -38, 69, 704, 1339, 2183, 3028, 2816, 2861, 2953, 2790, -349, + 96, -19, -134, -137, -140, -204, -268, -268, 1580, 1614, 1649, 1427, 1718, -300, -1293, -1007, -720, -443, -166, + 111, 388, 613, 1350, 1831, 2312, 2620, 2928, 3076, 3225, 3249, 3273, 3297, 3322, 3475, 3628, 3333, 1502, 655, 832, + 593, 3938, 4024, 4110, 4068, 4026, 3980, 3934, 3984, 4034, 3998, 3962, 3990, 4018, 3786, 3554, 3610, 3666, 3459, + 3253, 3111, 2969, 2858, 2236, -210, -96, -154, -212, -174, -136, -200, -264, -264, 1662, 1653, 1644, 1619, 1851, + -988, -1267, -986, -704, -402, -100, 10, 120, 404, 944, 1580, 2216, 2504, 2793, 2873, 2954, 2977, 2999, 3086, 3173, + 3238, 3303, 3576, 521, 554, 587, 1772, 3981, 4019, 4058, 4032, 4007, 3971, 3936, 3948, 3961, 3920, 3879, 3806, + 3989, 3866, 3743, 3636, 3529, 3375, 3222, 3069, 2916, 2907, 1362, -119, -64, -113, -162, -147, -132, -196, -260, + -260, 1744, 1692, 1640, 1556, 1472, -1932, -1240, -964, -688, -361, -34, 165, 364, 707, 1050, 1585, 2120, 2389, + 2658, 2671, 2684, 2705, 2726, 2875, 3024, 3001, 2978, 2283, 564, 965, 342, 2951, 4024, 4015, 4006, 3997, 3988, + 3963, 3938, 3913, 3888, 3842, 3796, 3622, 3960, 3946, 3932, 3662, 3392, 3292, 3192, 3028, 2864, 2956, 488, -28, + -32, -72, -112, -120, -128, -192, -256, -256, 1834, 1635, 1692, 1718, 207, -1664, -1230, -925, -619, -285, 50, 256, + 719, 706, 948, 1127, 1562, 1845, 2129, 2236, 2344, 2448, 2551, 2655, 2759, 2739, 2719, 1563, 663, 623, 327, 4207, + 3992, 4013, 4034, 3991, 3948, 3923, 3898, 3873, 3848, 3774, 3701, 3484, 3523, 3726, 3929, 3812, 3695, 3604, 3513, + 3407, 3300, 3349, -441, -232, -22, -48, -74, -100, -126, -174, -222, -222, 1924, 1578, 1745, 1880, -1057, -1395, + -1220, -885, -550, -208, 134, 92, 563, 449, 847, 669, 1004, 1302, 1600, 1802, 2005, 2191, 2377, 2435, 2494, 2477, + 2460, 843, 763, 794, 1337, 3928, 3960, 4011, 4062, 3985, 3908, 3883, 3858, 3833, 3808, 3707, 3607, 3603, 3599, + 3506, 3414, 3706, 3998, 3916, 3835, 3786, 3737, 2207, -346, 77, -12, -24, -36, -80, -124, -156, -188, -188, 1598, + 1585, 1830, 2154, -1874, -1414, -1210, -558, -417, -516, -102, 440, 214, 192, 682, 435, 702, 870, 1039, 1224, 1409, + 1710, 2011, 2039, 2069, 2087, 1849, 795, 766, 596, 2475, 3953, 3896, 3929, 3962, 3915, 3868, 3843, 3818, 3793, + 3768, 3688, 3609, 3577, 3546, 3462, 3379, 3312, 3245, 3364, 3485, 3189, 2893, 857, -155, 33, -34, -48, -62, -108, + -154, -154, -154, -154, 1784, 1849, 1915, 892, -1666, -1177, -1711, -742, -796, -823, 175, -748, 378, 191, 517, + 202, 400, 439, 479, 646, 814, 1229, 1645, 1644, 1644, 1697, 1239, 748, 770, 399, 3613, 3978, 3832, 3847, 3862, + 3845, 3828, 3803, 3778, 3753, 3728, 3669, 3611, 3552, 3494, 3419, 3345, 3174, 3004, 2813, 2623, 2592, 2562, -237, + 37, -10, -56, -72, -88, -136, -184, -152, -120, -120, 1802, 1900, 2255, -286, -1291, -1130, -713, -393, -327, -387, + -445, 200, -179, 436, 27, -46, -118, 203, 270, 384, 498, 686, 874, 998, 1123, 1253, 1128, 794, 717, 1161, 3654, + 3843, 3776, 3789, 3802, 3783, 3764, 3617, 3726, 3691, 3656, 3596, 3536, 3476, 3417, 3341, 3266, 3078, 2891, 2687, + 2484, 2617, 1982, -29, 8, 12, 18, -18, -54, 6, 66, -30, -126, -126, 1820, 1696, 2084, -2232, -1939, -571, -1763, + -1835, -1394, -462, -553, -388, -223, -1111, -462, -37, -124, -32, -451, -134, 183, 143, 104, 353, 602, 809, 1017, + 841, 665, 1924, 3696, 3708, 3720, 3731, 3742, 3721, 3700, 3431, 3674, 3629, 3584, 3523, 3462, 3401, 3341, 3264, + 3187, 2982, 2778, 2562, 2346, 2386, 891, -77, -21, 35, 92, 36, -20, -108, -196, -164, -132, -132, 1710, 1955, 1177, + -2834, -956, -2076, -2173, -365, -1885, -1353, -821, -1600, -844, -1250, -887, -653, -674, -555, -436, -636, -325, + -304, -282, -101, -175, 493, 906, 871, 580, 2767, 3674, 3653, 3632, 3657, 3682, 3627, 3572, 3437, 3558, 3535, 3512, + 3450, 3388, 3326, 3264, 3186, 3108, 2902, 2697, 2500, 2304, 2219, 343, 179, 270, 154, 38, -6, -50, -110, -170, + -154, -138, -138, 1600, 1959, -242, -2667, -2020, -2557, -2582, -1455, 696, 316, 960, 2052, 2120, 1940, 1760, 1292, + 824, -310, -932, -1394, -832, -750, -668, -298, -440, 434, 796, 902, 496, 3610, 3652, 3598, 3544, 3583, 3622, 3533, + 3444, 3443, 3442, 3441, 3440, 3377, 3314, 3251, 3188, 3109, 3030, 2823, 2616, 2439, 2262, 2053, -204, 179, 50, 17, + -16, -48, -80, -112, -144, -144, -144, -144, 1956, 1852, -2091, -3026, -1145, 322, 2045, 1672, 1555, 1328, 1614, + 1916, 1706, 1622, 1282, 1502, 1466, 1301, 1393, 940, -792, -1548, -769, -821, -617, 926, 934, 909, 1397, 3323, + 3456, 3446, 3436, 3393, 3351, 3388, 3426, 3374, 3321, 3445, 3313, 3265, 3217, 3153, 3090, 2998, 2906, 2686, 2467, + 2291, 2115, 1283, -61, 137, 79, 37, -5, -37, -69, -101, -133, -133, -133, -133, 1800, 1746, 669, 1992, 1779, 1665, + 1552, 1727, 1390, 1317, 1245, 1269, 1293, 1560, 1316, 1456, 1084, 1121, 1158, 971, 1297, 726, -869, -1344, -794, + 1419, 1072, 917, 2299, 3036, 3261, 3294, 3328, 3204, 3080, 3244, 3409, 3305, 3201, 3449, 3186, 3153, 3121, 3056, + 2992, 2887, 2783, 2550, 2318, 2143, 1968, 513, 82, 95, 108, 57, 6, -26, -58, -90, -122, -122, -122, -122, 1516, + 1832, 1637, 1905, 1406, 1344, 1283, 1590, 1641, 1466, 1292, 1277, 1263, 1386, 1254, 1314, 1118, 1116, 1115, 906, + 953, 1160, 1111, 117, -363, 807, 698, 701, 2240, 3325, 2362, 2934, 3252, 2998, 2745, 2924, 3103, 3156, 2953, 3277, + 3091, 3057, 3024, 2959, 2894, 2776, 2659, 2414, 2169, 2075, 1981, 255, 65, 69, 73, 45, 17, -15, -47, -79, -111, + -111, -111, -111, 1744, 1662, 1581, 1563, 1546, 1536, 1527, 1453, 1380, 1359, 1339, 1286, 1234, 1213, 1193, 1172, + 1152, 1112, 1073, 1097, 1122, 826, 1043, 1067, 1092, 964, 837, 741, 2182, 2078, 2487, 2831, 2664, 2793, 2923, 2860, + 2798, 3007, 2705, 3106, 2996, 2962, 2928, 2862, 2796, 2666, 2536, 2278, 2020, 1751, 1482, -259, 48, 43, 38, 33, 28, + -4, -36, -68, -100, -100, -100, -100, 1684, 1640, 1596, 1584, 1573, 1543, 1514, 1452, 1391, 1360, 1329, 1282, 1236, + 1213, 1191, 1168, 1146, 1107, 1070, 1064, 1058, 920, 1038, 996, 955, 924, 895, 881, 1635, 1679, 2235, 2439, 2132, + 2451, 2772, 2580, 2644, 2714, 2528, 2742, 2701, 2828, 2699, 2570, 2442, 2383, 2324, 2105, 1887, 1733, 811, -79, 55, + 63, 71, 47, 23, -7, -37, -67, -97, -113, -129, -129, 1624, 1618, 1612, 1606, 1601, 1551, 1501, 1451, 1402, 1361, + 1320, 1279, 1239, 1214, 1189, 1164, 1140, 1103, 1067, 1031, 995, 1014, 1034, 926, 818, 885, 953, 1021, 1089, 1024, + 1472, 2048, 2112, 2110, 2109, 2044, 2491, 2421, 2352, 2379, 2406, 2694, 2471, 2279, 2088, 2100, 2113, 1933, 1754, + 1715, 140, 101, 62, 83, 104, 61, 18, -10, -38, -66, -94, -126, -158, -158, 1724, 1788, 1852, 1692, 1532, 1494, + 1456, 1418, 1381, 1346, 1311, 1276, 1241, 1214, 1187, 1160, 1134, 1099, 1064, 1030, 995, 996, 998, 935, 873, 878, + 883, 793, 702, 657, 1125, 1832, 2284, 1193, 1638, 1796, 2209, 2320, 2176, 2239, 2047, 2560, 2562, 1892, 1734, 1673, + 1613, 1745, 1621, 1153, -83, -7, 69, 71, 73, 43, 13, -13, -39, -65, -91, -139, -187, -187, 1824, 1702, 1580, 1522, + 1464, 1438, 1412, 1386, 1360, 1331, 1302, 1273, 1244, 1215, 1186, 1157, 1128, 1095, 1062, 1029, 996, 979, 962, 945, + 928, 871, 814, 821, 828, 803, 1290, 1617, 1944, 2068, 1168, 1292, 1416, 1708, 1488, 1844, 1688, 2171, 2142, 1249, + 1380, 1503, 1626, 1045, -48, 79, 206, 141, 76, 59, 42, 25, 8, -16, -40, -64, -88, -152, -216, -216, 1688, 1615, + 1542, 1501, 1460, 1429, 1398, 1367, 1336, 1310, 1284, 1258, 1232, 1206, 1180, 1154, 1128, 1093, 1058, 1023, 988, + 969, 950, 931, 912, 862, 812, 794, 776, 596, 672, 972, 1272, 330, 924, 1038, 1152, 1298, 1444, 1910, 1608, 1532, + 1200, 516, 344, 260, 176, 252, 72, 123, 174, 129, 84, 65, 46, 27, 8, -18, -44, -70, -96, -144, -192, -192, 1552, + 1528, 1504, 1480, 1456, 1420, 1384, 1348, 1312, 1289, 1266, 1243, 1220, 1197, 1174, 1151, 1128, 1091, 1054, 1017, + 980, 959, 938, 917, 896, 853, 810, 767, 724, 645, 566, 583, 600, 640, 680, 528, 376, 376, 888, 1464, 1016, 637, + 258, 295, 332, 297, 262, 227, 192, 167, 142, 117, 92, 71, 50, 29, 8, -20, -48, -76, -104, -136, -168, -168, 1544, + 1521, 1498, 1475, 1452, 1411, 1370, 1329, 1288, 1268, 1248, 1228, 1208, 1188, 1168, 1148, 1128, 1089, 1050, 1011, + 972, 949, 926, 903, 880, 844, 808, 772, 736, 678, 620, 610, 600, 614, 628, 546, 464, 238, 2060, 1690, 1576, 1710, + 308, 314, 320, 286, 252, 218, 184, 163, 142, 121, 100, 77, 54, 31, 8, -22, -52, -82, -112, -128, -144, -144, 1536, + 1514, 1492, 1470, 1448, 1402, 1356, 1310, 1264, 1247, 1230, 1213, 1196, 1179, 1162, 1145, 1128, 1087, 1046, 1005, + 964, 939, 914, 889, 864, 835, 806, 777, 748, 711, 674, 637, 600, 588, 576, 564, 552, 612, 160, 1916, 1112, 223, + 358, 333, 308, 275, 242, 209, 176, 159, 142, 125, 108, 83, 58, 33, 8, -24, -56, -88, -120, -120, -120, -120, 1536, + 1514, 1492, 1470, 1448, 1402, 1356, 1310, 1264, 1247, 1230, 1213, 1196, 1179, 1162, 1145, 1128, 1087, 1046, 1005, + 964, 939, 914, 889, 864, 835, 806, 777, 748, 711, 674, 637, 600, 588, 576, 564, 552, 644, 480, 108, 504, 159, 326, + 317, 308, 275, 242, 209, 176, 159, 142, 125, 108, 83, 58, 33, 8, -24, -56, -88, -120, -120, -120, -120, 1536, 1514, + 1492, 1470, 1448, 1402, 1356, 1310, 1264, 1247, 1230, 1213, 1196, 1179, 1162, 1145, 1128, 1087, 1046, 1005, 964, + 939, 914, 889, 864, 835, 806, 777, 748, 711, 674, 637, 600, 588, 576, 564, 552, 420, 288, 348, 408, 351, 294, 301, + 308, 275, 242, 209, 176, 159, 142, 125, 108, 83, 58, 33, 8, -24, -56, -88, -120, -120, -120, -120, 1536, 1514, + 1492, 1470, 1448, 1402, 1356, 1310, 1264, 1247, 1230, 1213, 1196, 1179, 1162, 1145, 1128, 1087, 1046, 1005, 964, + 939, 914, 889, 864, 835, 806, 777, 748, 711, 674, 637, 600, 588, 576, 564, 552, 420, 288, 348, 408, 351, 294, 301, + 308, 275, 242, 209, 176, 159, 142, 125, 108, 83, 58, 33, 8, -24, -56, -88, -120, -120, -120, -120, +]; diff --git a/crates/ironrdp-testsuite-core/tests/graphics/image_processing.rs b/crates/ironrdp-testsuite-core/tests/graphics/image_processing.rs new file mode 100644 index 00000000..2adc781f --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/graphics/image_processing.rs @@ -0,0 +1,2716 @@ +use std::io; + +use ironrdp_graphics::image_processing::*; +use ironrdp_pdu::geometry::InclusiveRectangle; +use proptest::prelude::*; + +fn bgra_to_rgba(input: &[u8], mut output: &mut [u8]) -> io::Result<()> { + use std::io::Write as _; + + for chunk in input.chunks(4) { + let b = chunk[0]; + let g = chunk[1]; + let r = chunk[2]; + let a = chunk[3]; + output.write_all(&[r, g, b, a])?; + } + + Ok(()) +} + +#[test] +fn image_region_copy_bgra32_to_rgba32() { + proptest!(|(source_buffer in proptest::collection::vec(any::(), 8 * 8 * 4))| { + let source_region = ImageRegion { + region: InclusiveRectangle { + left: 0, + top: 0, + right: 7, + bottom: 7, + }, + step: 32, + pixel_format: PixelFormat::BgrA32, + data: &source_buffer, + }; + + let mut destination_buffer = vec![0; 8 * 8 * 4]; + let mut destination_region = ImageRegionMut { + region: InclusiveRectangle { + left: 0, + top: 0, + right: 7, + bottom: 7, + }, + step: 32, + pixel_format: PixelFormat::RgbA32, + data: &mut destination_buffer, + }; + source_region.copy_to(&mut destination_region).unwrap(); + + let mut expected = vec![0; 8 * 8 * 4]; + bgra_to_rgba(&source_buffer, &mut expected).unwrap(); + + prop_assert_eq!(destination_buffer, expected); + }) +} + +#[test] +fn image_region_correctly_writes_image_with_different_formats_with_same_sizes() { + let mut destination_data = vec![0; CONVERTED_TO_XRGB_BUFFER.len()]; + let source_region = ImageRegion { + region: InclusiveRectangle { + left: 0, + top: 0, + right: 63, + bottom: 63, + }, + step: 256, + pixel_format: PixelFormat::BgrX32, + data: &SOURCE_IN_RGBX_BUFFER, + }; + let mut destination_region = ImageRegionMut { + region: InclusiveRectangle { + left: 0, + top: 0, + right: 63, + bottom: 63, + }, + step: 256, + pixel_format: PixelFormat::XRgb32, + data: destination_data.as_mut_slice(), + }; + + source_region.copy_to(&mut destination_region).unwrap(); + assert_eq!(CONVERTED_TO_XRGB_BUFFER.as_ref(), destination_data.as_slice()); +} + +#[test] +fn image_region_correctly_writes_image_with_same_formats_and_different_sizes() { + let mut destination_data = vec![0; CONVERTED_TO_XRGB_BUFFER.len()]; + let source_region = ImageRegion { + region: InclusiveRectangle { + left: 8, + top: 8, + right: 63, + bottom: 63, + }, + step: 256, + pixel_format: PixelFormat::BgrX32, + data: &SOURCE_IN_RGBX_BUFFER, + }; + let mut destination_region = ImageRegionMut { + region: InclusiveRectangle { + left: 8, + top: 8, + right: 39, + bottom: 23, + }, + step: 256, + pixel_format: PixelFormat::BgrX32, + data: destination_data.as_mut_slice(), + }; + + source_region.copy_to(&mut destination_region).unwrap(); + assert_eq!( + CONVERTED_TO_BGRX_WITH_PARTIALLY_COVERED_RECTANGLE_BUFFER.as_ref(), + destination_data.as_slice() + ); +} + +const SOURCE_IN_RGBX_BUFFER: [u8; 64 * 64 * 4] = [ + 0xDE, 0x9B, 0x22, 0xFF, 0xE0, 0x9D, 0x23, 0xFF, 0xE1, 0x9E, 0x25, 0xFF, 0xE8, 0xA5, 0x2B, 0xFF, 0xDF, 0x9B, 0x22, + 0xFF, 0xDF, 0x9C, 0x22, 0xFF, 0xE0, 0x9C, 0x22, 0xFF, 0xDF, 0x9C, 0x22, 0xFF, 0xDF, 0x9B, 0x21, 0xFF, 0xDF, 0x9B, + 0x22, 0xFF, 0xDF, 0x9B, 0x23, 0xFF, 0xDF, 0x9B, 0x23, 0xFF, 0xDF, 0x9C, 0x24, 0xFF, 0xE2, 0x9B, 0x21, 0xFF, 0xE5, + 0x9B, 0x1D, 0xFF, 0xE1, 0x9A, 0x1F, 0xFF, 0xDD, 0x98, 0x21, 0xFF, 0xDE, 0x99, 0x21, 0xFF, 0xDE, 0x99, 0x20, 0xFF, + 0xDF, 0x9A, 0x1F, 0xFF, 0xE0, 0x9A, 0x1F, 0xFF, 0xE0, 0x99, 0x1E, 0xFF, 0xDF, 0x99, 0x1D, 0xFF, 0xDF, 0x98, 0x1C, + 0xFF, 0xDF, 0x97, 0x1B, 0xFF, 0xDC, 0x95, 0x1E, 0xFF, 0xD8, 0x93, 0x21, 0xFF, 0xDC, 0x93, 0x1F, 0xFF, 0xE0, 0x93, + 0x1C, 0xFF, 0xDC, 0x94, 0x1A, 0xFF, 0xD8, 0x95, 0x18, 0xFF, 0xDB, 0x91, 0x1C, 0xFF, 0xDE, 0x8E, 0x1F, 0xFF, 0xDE, + 0x90, 0x1A, 0xFF, 0xDE, 0x93, 0x16, 0xFF, 0xDF, 0x92, 0x17, 0xFF, 0xDF, 0x91, 0x18, 0xFF, 0xDF, 0x90, 0x17, 0xFF, + 0xDE, 0x8F, 0x17, 0xFF, 0xDE, 0x8E, 0x16, 0xFF, 0xDE, 0x8C, 0x15, 0xFF, 0xDD, 0x8C, 0x14, 0xFF, 0xDB, 0x8C, 0x13, + 0xFF, 0xDA, 0x8C, 0x12, 0xFF, 0xD9, 0x8C, 0x11, 0xFF, 0xD9, 0x8B, 0x11, 0xFF, 0xD9, 0x89, 0x11, 0xFF, 0xDA, 0x88, + 0x11, 0xFF, 0xDA, 0x87, 0x12, 0xFF, 0xDA, 0x86, 0x11, 0xFF, 0xDA, 0x86, 0x10, 0xFF, 0xD9, 0x85, 0x10, 0xFF, 0xD9, + 0x84, 0x0F, 0xFF, 0xD9, 0x83, 0x0E, 0xFF, 0xD8, 0x83, 0x0E, 0xFF, 0xD8, 0x82, 0x0D, 0xFF, 0xD8, 0x81, 0x0C, 0xFF, + 0xD7, 0x80, 0x0C, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xD6, 0x7F, 0x0D, 0xFF, 0xD6, 0x7E, 0x0D, 0xFF, 0xD6, 0x7E, 0x0D, + 0xFF, 0xD6, 0x7E, 0x0D, 0xFF, 0xD6, 0x7E, 0x0D, 0xFF, 0xE0, 0x9F, 0x24, 0xFF, 0xE1, 0xA0, 0x27, 0xFF, 0xE2, 0xA2, + 0x29, 0xFF, 0xE5, 0xA4, 0x2A, 0xFF, 0xE0, 0x9E, 0x24, 0xFF, 0xE1, 0x9E, 0x24, 0xFF, 0xE1, 0x9E, 0x24, 0xFF, 0xE1, + 0x9E, 0x23, 0xFF, 0xE1, 0x9D, 0x23, 0xFF, 0xE1, 0x9D, 0x23, 0xFF, 0xE1, 0x9D, 0x24, 0xFF, 0xE1, 0x9D, 0x24, 0xFF, + 0xE1, 0x9D, 0x25, 0xFF, 0xE1, 0x9D, 0x23, 0xFF, 0xE2, 0x9C, 0x22, 0xFF, 0xE0, 0x9C, 0x22, 0xFF, 0xDF, 0x9B, 0x22, + 0xFF, 0xE0, 0x9B, 0x21, 0xFF, 0xE1, 0x9B, 0x20, 0xFF, 0xE1, 0x9B, 0x20, 0xFF, 0xE1, 0x9B, 0x1F, 0xFF, 0xDF, 0x9A, + 0x20, 0xFF, 0xDE, 0x99, 0x20, 0xFF, 0xDE, 0x98, 0x1E, 0xFF, 0xDF, 0x97, 0x1D, 0xFF, 0xDF, 0x97, 0x1D, 0xFF, 0xDF, + 0x96, 0x1E, 0xFF, 0xDF, 0x95, 0x1D, 0xFF, 0xDE, 0x94, 0x1C, 0xFF, 0xDF, 0x94, 0x1C, 0xFF, 0xE0, 0x93, 0x1B, 0xFF, + 0xE0, 0x93, 0x1C, 0xFF, 0xE0, 0x92, 0x1D, 0xFF, 0xDE, 0x93, 0x1B, 0xFF, 0xDC, 0x94, 0x19, 0xFF, 0xDE, 0x93, 0x19, + 0xFF, 0xE0, 0x92, 0x19, 0xFF, 0xDF, 0x91, 0x19, 0xFF, 0xDF, 0x90, 0x18, 0xFF, 0xDF, 0x8F, 0x17, 0xFF, 0xDF, 0x8E, + 0x17, 0xFF, 0xDE, 0x8E, 0x16, 0xFF, 0xDD, 0x8D, 0x15, 0xFF, 0xDC, 0x8D, 0x13, 0xFF, 0xDB, 0x8D, 0x12, 0xFF, 0xDB, + 0x8C, 0x12, 0xFF, 0xDB, 0x8B, 0x12, 0xFF, 0xDB, 0x89, 0x12, 0xFF, 0xDB, 0x88, 0x12, 0xFF, 0xDB, 0x87, 0x11, 0xFF, + 0xDB, 0x87, 0x11, 0xFF, 0xDB, 0x86, 0x10, 0xFF, 0xDB, 0x85, 0x0F, 0xFF, 0xDA, 0x84, 0x0E, 0xFF, 0xD9, 0x83, 0x0D, + 0xFF, 0xD9, 0x83, 0x0D, 0xFF, 0xD9, 0x83, 0x0D, 0xFF, 0xD8, 0x82, 0x0D, 0xFF, 0xD8, 0x81, 0x0D, 0xFF, 0xD7, 0x80, + 0x0D, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xE2, + 0xA2, 0x27, 0xFF, 0xE3, 0xA4, 0x2A, 0xFF, 0xE3, 0xA5, 0x2D, 0xFF, 0xE3, 0xA3, 0x29, 0xFF, 0xE2, 0xA1, 0x26, 0xFF, + 0xE2, 0xA1, 0x25, 0xFF, 0xE2, 0xA1, 0x25, 0xFF, 0xE2, 0xA0, 0x25, 0xFF, 0xE2, 0xA0, 0x24, 0xFF, 0xE2, 0x9F, 0x25, + 0xFF, 0xE3, 0x9F, 0x25, 0xFF, 0xE3, 0x9E, 0x25, 0xFF, 0xE3, 0x9E, 0x26, 0xFF, 0xE1, 0x9E, 0x26, 0xFF, 0xDE, 0x9D, + 0x27, 0xFF, 0xDF, 0x9D, 0x24, 0xFF, 0xE1, 0x9E, 0x22, 0xFF, 0xE2, 0x9D, 0x21, 0xFF, 0xE3, 0x9D, 0x20, 0xFF, 0xE3, + 0x9D, 0x20, 0xFF, 0xE3, 0x9C, 0x20, 0xFF, 0xDF, 0x9B, 0x22, 0xFF, 0xDC, 0x99, 0x24, 0xFF, 0xDE, 0x98, 0x21, 0xFF, + 0xE0, 0x98, 0x1F, 0xFF, 0xE3, 0x99, 0x1D, 0xFF, 0xE7, 0x9A, 0x1B, 0xFF, 0xE1, 0x98, 0x1B, 0xFF, 0xDC, 0x96, 0x1C, + 0xFF, 0xE2, 0x94, 0x1D, 0xFF, 0xE9, 0x92, 0x1F, 0xFF, 0xE5, 0x94, 0x1D, 0xFF, 0xE2, 0x96, 0x1A, 0xFF, 0xDE, 0x95, + 0x1B, 0xFF, 0xDA, 0x95, 0x1D, 0xFF, 0xDD, 0x94, 0x1C, 0xFF, 0xE0, 0x93, 0x1A, 0xFF, 0xE0, 0x92, 0x1A, 0xFF, 0xE0, + 0x91, 0x19, 0xFF, 0xDF, 0x91, 0x19, 0xFF, 0xDF, 0x90, 0x18, 0xFF, 0xDE, 0x8F, 0x17, 0xFF, 0xDE, 0x8F, 0x16, 0xFF, + 0xDD, 0x8E, 0x15, 0xFF, 0xDD, 0x8E, 0x14, 0xFF, 0xDC, 0x8D, 0x14, 0xFF, 0xDC, 0x8C, 0x13, 0xFF, 0xDC, 0x8B, 0x12, + 0xFF, 0xDB, 0x8A, 0x12, 0xFF, 0xDC, 0x89, 0x11, 0xFF, 0xDC, 0x88, 0x11, 0xFF, 0xDC, 0x87, 0x10, 0xFF, 0xDC, 0x86, + 0x10, 0xFF, 0xDB, 0x84, 0x0E, 0xFF, 0xD9, 0x83, 0x0D, 0xFF, 0xD9, 0x83, 0x0E, 0xFF, 0xDA, 0x84, 0x0E, 0xFF, 0xD9, + 0x83, 0x0E, 0xFF, 0xD9, 0x82, 0x0E, 0xFF, 0xD8, 0x80, 0x0D, 0xFF, 0xD8, 0x7F, 0x0D, 0xFF, 0xD8, 0x7F, 0x0D, 0xFF, + 0xD8, 0x7F, 0x0D, 0xFF, 0xD8, 0x7F, 0x0D, 0xFF, 0xE4, 0xA6, 0x29, 0xFF, 0xE3, 0xA7, 0x2D, 0xFF, 0xE3, 0xA8, 0x30, + 0xFF, 0xE3, 0xA6, 0x2C, 0xFF, 0xE3, 0xA3, 0x27, 0xFF, 0xE3, 0xA3, 0x27, 0xFF, 0xE3, 0xA3, 0x26, 0xFF, 0xE4, 0xA2, + 0x26, 0xFF, 0xE4, 0xA2, 0x26, 0xFF, 0xE4, 0xA1, 0x26, 0xFF, 0xE4, 0xA1, 0x26, 0xFF, 0xE5, 0xA0, 0x26, 0xFF, 0xE5, + 0x9F, 0x26, 0xFF, 0xE4, 0xA0, 0x25, 0xFF, 0xE4, 0xA0, 0x24, 0xFF, 0xE3, 0x9F, 0x24, 0xFF, 0xE3, 0x9E, 0x24, 0xFF, + 0xE4, 0x9E, 0x23, 0xFF, 0xE6, 0x9F, 0x21, 0xFF, 0xE5, 0x9F, 0x21, 0xFF, 0xE3, 0x9E, 0x22, 0xFF, 0xE5, 0xA4, 0x13, + 0xFF, 0xE7, 0x9F, 0x1A, 0xFF, 0xE7, 0x9F, 0x15, 0xFF, 0xE7, 0xA0, 0x10, 0xFF, 0xEF, 0x9F, 0x11, 0xFF, 0xF7, 0x9E, + 0x12, 0xFF, 0xEC, 0x99, 0x1A, 0xFF, 0xE1, 0x9A, 0x17, 0xFF, 0xE3, 0x9C, 0x14, 0xFF, 0xE5, 0x98, 0x1C, 0xFF, 0xE6, + 0x97, 0x1C, 0xFF, 0xE6, 0x96, 0x1B, 0xFF, 0xDB, 0x98, 0x1B, 0xFF, 0xDF, 0x96, 0x1C, 0xFF, 0xE0, 0x95, 0x1C, 0xFF, + 0xE1, 0x94, 0x1B, 0xFF, 0xE1, 0x93, 0x1B, 0xFF, 0xE0, 0x93, 0x1A, 0xFF, 0xE0, 0x92, 0x1A, 0xFF, 0xE0, 0x92, 0x19, + 0xFF, 0xDF, 0x91, 0x18, 0xFF, 0xDF, 0x90, 0x18, 0xFF, 0xDF, 0x8F, 0x17, 0xFF, 0xDF, 0x8F, 0x16, 0xFF, 0xDE, 0x8E, + 0x15, 0xFF, 0xDD, 0x8D, 0x14, 0xFF, 0xDD, 0x8C, 0x13, 0xFF, 0xDC, 0x8B, 0x12, 0xFF, 0xDC, 0x8A, 0x12, 0xFF, 0xDD, + 0x89, 0x11, 0xFF, 0xDD, 0x87, 0x11, 0xFF, 0xDE, 0x86, 0x10, 0xFF, 0xDC, 0x85, 0x0F, 0xFF, 0xD9, 0x83, 0x0D, 0xFF, + 0xDA, 0x84, 0x0E, 0xFF, 0xDB, 0x85, 0x0F, 0xFF, 0xDA, 0x84, 0x0F, 0xFF, 0xDA, 0x83, 0x0E, 0xFF, 0xDA, 0x81, 0x0E, + 0xFF, 0xD9, 0x80, 0x0D, 0xFF, 0xD9, 0x80, 0x0D, 0xFF, 0xD9, 0x80, 0x0D, 0xFF, 0xD9, 0x80, 0x0D, 0xFF, 0xE7, 0xAA, + 0x2C, 0xFF, 0xE4, 0xAA, 0x30, 0xFF, 0xE2, 0xAA, 0x33, 0xFF, 0xE3, 0xA8, 0x2E, 0xFF, 0xE4, 0xA5, 0x28, 0xFF, 0xE5, + 0xA5, 0x28, 0xFF, 0xE5, 0xA5, 0x28, 0xFF, 0xE5, 0xA4, 0x28, 0xFF, 0xE5, 0xA4, 0x27, 0xFF, 0xE6, 0xA3, 0x27, 0xFF, + 0xE6, 0xA2, 0x27, 0xFF, 0xE7, 0xA1, 0x27, 0xFF, 0xE7, 0xA1, 0x27, 0xFF, 0xE8, 0xA2, 0x25, 0xFF, 0xE9, 0xA3, 0x22, + 0xFF, 0xE7, 0xA0, 0x24, 0xFF, 0xE6, 0x9E, 0x27, 0xFF, 0xE7, 0x9F, 0x25, 0xFF, 0xE8, 0xA0, 0x22, 0xFF, 0xF4, 0xA3, + 0x18, 0xFF, 0xFF, 0xA7, 0x0D, 0xFF, 0xDD, 0xA5, 0x1A, 0xFF, 0xBA, 0x8D, 0x54, 0xFF, 0x9C, 0x83, 0x6E, 0xFF, 0x7D, + 0x79, 0x88, 0xFF, 0x7B, 0x79, 0x8C, 0xFF, 0x79, 0x79, 0x91, 0xFF, 0x94, 0x7A, 0x7E, 0xFF, 0xAF, 0x87, 0x55, 0xFF, + 0xD6, 0x9B, 0x21, 0xFF, 0xFD, 0xA3, 0x04, 0xFF, 0xF4, 0x9D, 0x0F, 0xFF, 0xEB, 0x96, 0x1B, 0xFF, 0xD9, 0x9A, 0x1B, + 0xFF, 0xE4, 0x98, 0x1B, 0xFF, 0xE3, 0x96, 0x1C, 0xFF, 0xE2, 0x95, 0x1C, 0xFF, 0xE2, 0x94, 0x1C, 0xFF, 0xE1, 0x94, + 0x1B, 0xFF, 0xE1, 0x94, 0x1B, 0xFF, 0xE0, 0x93, 0x1B, 0xFF, 0xE0, 0x92, 0x1A, 0xFF, 0xE0, 0x91, 0x19, 0xFF, 0xE1, + 0x90, 0x18, 0xFF, 0xE1, 0x8F, 0x18, 0xFF, 0xE0, 0x8F, 0x16, 0xFF, 0xDF, 0x8E, 0x15, 0xFF, 0xDE, 0x8D, 0x14, 0xFF, + 0xDC, 0x8C, 0x12, 0xFF, 0xDD, 0x8B, 0x12, 0xFF, 0xDE, 0x8A, 0x12, 0xFF, 0xDF, 0x88, 0x11, 0xFF, 0xE0, 0x87, 0x11, + 0xFF, 0xDD, 0x85, 0x0F, 0xFF, 0xDA, 0x83, 0x0D, 0xFF, 0xDB, 0x85, 0x0E, 0xFF, 0xDC, 0x87, 0x10, 0xFF, 0xDC, 0x85, + 0x0F, 0xFF, 0xDB, 0x84, 0x0F, 0xFF, 0xDB, 0x82, 0x0E, 0xFF, 0xDA, 0x81, 0x0D, 0xFF, 0xDA, 0x81, 0x0D, 0xFF, 0xDA, + 0x81, 0x0D, 0xFF, 0xDA, 0x81, 0x0D, 0xFF, 0xE4, 0xAA, 0x30, 0xFF, 0xE8, 0xAF, 0x35, 0xFF, 0xE3, 0xAB, 0x33, 0xFF, + 0xE5, 0xA9, 0x2F, 0xFF, 0xE6, 0xA8, 0x2A, 0xFF, 0xE8, 0xAD, 0x35, 0xFF, 0xE7, 0xA6, 0x25, 0xFF, 0xE7, 0xA7, 0x28, + 0xFF, 0xE7, 0xA8, 0x2B, 0xFF, 0xE5, 0xA6, 0x2D, 0xFF, 0xE4, 0xA4, 0x2E, 0xFF, 0xE6, 0xA4, 0x2B, 0xFF, 0xE8, 0xA4, + 0x29, 0xFF, 0xE5, 0xA4, 0x2A, 0xFF, 0xE1, 0xA5, 0x2C, 0xFF, 0xEF, 0xA9, 0x10, 0xFF, 0xF6, 0xAD, 0x12, 0xFF, 0xF8, + 0xA2, 0x22, 0xFF, 0xA5, 0x91, 0x60, 0xFF, 0x5C, 0x75, 0xA5, 0xFF, 0x14, 0x59, 0xEB, 0xFF, 0x0C, 0x48, 0xFF, 0xFF, + 0x03, 0x55, 0xFA, 0xFF, 0x0F, 0x59, 0xFF, 0xFF, 0x1A, 0x5D, 0xFF, 0xFF, 0x16, 0x60, 0xFF, 0xFF, 0x11, 0x64, 0xF9, + 0xFF, 0x0F, 0x54, 0xFF, 0xFF, 0x0C, 0x4A, 0xFF, 0xFF, 0x17, 0x49, 0xFA, 0xFF, 0x23, 0x47, 0xF5, 0xFF, 0x7E, 0x72, + 0x8D, 0xFF, 0xD9, 0x9D, 0x26, 0xFF, 0xFF, 0xA1, 0x05, 0xFF, 0xE1, 0x96, 0x1D, 0xFF, 0xE9, 0x98, 0x17, 0xFF, 0xE3, + 0x97, 0x1C, 0xFF, 0xE3, 0x97, 0x1A, 0xFF, 0xE4, 0x97, 0x18, 0xFF, 0xE3, 0x96, 0x19, 0xFF, 0xE2, 0x94, 0x1B, 0xFF, + 0xE1, 0x93, 0x1A, 0xFF, 0xE0, 0x93, 0x19, 0xFF, 0xE1, 0x92, 0x18, 0xFF, 0xE1, 0x91, 0x17, 0xFF, 0xE0, 0x90, 0x16, + 0xFF, 0xDF, 0x8F, 0x15, 0xFF, 0xDE, 0x8E, 0x14, 0xFF, 0xDD, 0x8D, 0x13, 0xFF, 0xDE, 0x8D, 0x13, 0xFF, 0xDF, 0x8C, + 0x13, 0xFF, 0xDF, 0x8A, 0x12, 0xFF, 0xE0, 0x89, 0x10, 0xFF, 0xDD, 0x87, 0x0F, 0xFF, 0xDB, 0x84, 0x0E, 0xFF, 0xDF, + 0x8A, 0x13, 0xFF, 0xDB, 0x87, 0x0F, 0xFF, 0xDC, 0x86, 0x0F, 0xFF, 0xDC, 0x85, 0x0F, 0xFF, 0xDB, 0x84, 0x0E, 0xFF, + 0xDB, 0x82, 0x0D, 0xFF, 0xDB, 0x82, 0x0D, 0xFF, 0xDB, 0x82, 0x0D, 0xFF, 0xDB, 0x82, 0x0D, 0xFF, 0xE2, 0xAB, 0x33, + 0xFF, 0xEB, 0xB3, 0x3B, 0xFF, 0xE5, 0xAC, 0x33, 0xFF, 0xE6, 0xAB, 0x30, 0xFF, 0xE7, 0xAA, 0x2D, 0xFF, 0xEA, 0xB6, + 0x43, 0xFF, 0xEA, 0xA7, 0x23, 0xFF, 0xE9, 0xA9, 0x29, 0xFF, 0xE9, 0xAB, 0x2F, 0xFF, 0xE5, 0xA9, 0x32, 0xFF, 0xE2, + 0xA7, 0x35, 0xFF, 0xE6, 0xA7, 0x30, 0xFF, 0xEA, 0xA8, 0x2A, 0xFF, 0xF0, 0xAA, 0x25, 0xFF, 0xF6, 0xAD, 0x1F, 0xFF, + 0xA7, 0x8A, 0x4D, 0xFF, 0x4C, 0x66, 0xB7, 0xFF, 0x0F, 0x54, 0xFF, 0xFF, 0x0C, 0x64, 0xF7, 0xFF, 0x13, 0x63, 0xF8, + 0xFF, 0x1A, 0x61, 0xF9, 0xFF, 0x1E, 0x67, 0xEF, 0xFF, 0x22, 0x61, 0xFC, 0xFF, 0x25, 0x68, 0xFA, 0xFF, 0x28, 0x6F, + 0xF9, 0xFF, 0x22, 0x70, 0xF5, 0xFF, 0x1B, 0x72, 0xF2, 0xFF, 0x1F, 0x6B, 0xF2, 0xFF, 0x24, 0x64, 0xF1, 0xFF, 0x21, + 0x55, 0xFF, 0xFF, 0x1E, 0x53, 0xFF, 0xFF, 0x16, 0x4B, 0xFF, 0xFF, 0x0E, 0x43, 0xFF, 0xFF, 0x5A, 0x61, 0xB1, 0xFF, + 0xDF, 0x95, 0x1E, 0xFF, 0xF0, 0x9A, 0x12, 0xFF, 0xE5, 0x9A, 0x1B, 0xFF, 0xE5, 0x9A, 0x18, 0xFF, 0xE6, 0x9A, 0x14, + 0xFF, 0xE5, 0x98, 0x17, 0xFF, 0xE4, 0x95, 0x1B, 0xFF, 0xE2, 0x95, 0x1A, 0xFF, 0xE0, 0x94, 0x19, 0xFF, 0xE1, 0x93, + 0x18, 0xFF, 0xE2, 0x92, 0x17, 0xFF, 0xE1, 0x91, 0x16, 0xFF, 0xE0, 0x90, 0x16, 0xFF, 0xDF, 0x8F, 0x15, 0xFF, 0xDE, + 0x8F, 0x14, 0xFF, 0xDF, 0x8E, 0x14, 0xFF, 0xE1, 0x8E, 0x14, 0xFF, 0xE0, 0x8C, 0x12, 0xFF, 0xE0, 0x8A, 0x10, 0xFF, + 0xDE, 0x88, 0x10, 0xFF, 0xDC, 0x86, 0x10, 0xFF, 0xE3, 0x8E, 0x17, 0xFF, 0xDB, 0x87, 0x0D, 0xFF, 0xDB, 0x86, 0x0E, + 0xFF, 0xDC, 0x86, 0x0F, 0xFF, 0xDC, 0x85, 0x0E, 0xFF, 0xDB, 0x83, 0x0E, 0xFF, 0xDB, 0x83, 0x0E, 0xFF, 0xDB, 0x83, + 0x0E, 0xFF, 0xDB, 0x83, 0x0E, 0xFF, 0xEA, 0xB0, 0x36, 0xFF, 0xEF, 0xB3, 0x36, 0xFF, 0xED, 0xAE, 0x2E, 0xFF, 0xEC, + 0xAD, 0x2C, 0xFF, 0xEB, 0xAD, 0x2A, 0xFF, 0xEF, 0xB3, 0x40, 0xFF, 0xE9, 0xAA, 0x28, 0xFF, 0xE7, 0xAB, 0x2B, 0xFF, + 0xE6, 0xAB, 0x2F, 0xFF, 0xE6, 0xAA, 0x30, 0xFF, 0xE5, 0xAA, 0x31, 0xFF, 0xE6, 0xA9, 0x2E, 0xFF, 0xE7, 0xA9, 0x2B, + 0xFF, 0xEB, 0xA7, 0x24, 0xFF, 0x5F, 0x6A, 0x93, 0xFF, 0x05, 0x3D, 0xFF, 0xFF, 0x17, 0x56, 0xF9, 0xFF, 0x12, 0x72, + 0xE2, 0xFF, 0x29, 0x72, 0xF8, 0xFF, 0x27, 0x74, 0xF7, 0xFF, 0x25, 0x76, 0xF6, 0xFF, 0x28, 0x76, 0xF1, 0xFF, 0x2A, + 0x70, 0xF8, 0xFF, 0x2D, 0x77, 0xF8, 0xFF, 0x30, 0x7D, 0xF9, 0xFF, 0x2D, 0x7F, 0xF7, 0xFF, 0x2A, 0x81, 0xF5, 0xFF, + 0x2B, 0x7B, 0xF5, 0xFF, 0x2C, 0x75, 0xF5, 0xFF, 0x2B, 0x6A, 0xFD, 0xFF, 0x2A, 0x64, 0xFA, 0xFF, 0x2C, 0x5D, 0xF5, + 0xFF, 0x2E, 0x57, 0xF0, 0xFF, 0x10, 0x48, 0xFF, 0xFF, 0x0E, 0x45, 0xFF, 0xFF, 0x7F, 0x76, 0x80, 0xFF, 0xF0, 0xA7, + 0x02, 0xFF, 0xEA, 0x95, 0x24, 0xFF, 0xE3, 0x9A, 0x19, 0xFF, 0xE4, 0x98, 0x1B, 0xFF, 0xE4, 0x95, 0x1D, 0xFF, 0xE2, + 0x95, 0x1B, 0xFF, 0xDF, 0x96, 0x19, 0xFF, 0xE1, 0x94, 0x18, 0xFF, 0xE2, 0x93, 0x17, 0xFF, 0xE2, 0x92, 0x16, 0xFF, + 0xE1, 0x92, 0x16, 0xFF, 0xE0, 0x91, 0x15, 0xFF, 0xDF, 0x90, 0x15, 0xFF, 0xE0, 0x90, 0x15, 0xFF, 0xE2, 0x91, 0x15, + 0xFF, 0xE1, 0x8E, 0x12, 0xFF, 0xDF, 0x8C, 0x0F, 0xFF, 0xDF, 0x8B, 0x12, 0xFF, 0xDF, 0x8A, 0x14, 0xFF, 0xE2, 0x8D, + 0x15, 0xFF, 0xDC, 0x89, 0x0E, 0xFF, 0xDC, 0x88, 0x0E, 0xFF, 0xDD, 0x87, 0x0F, 0xFF, 0xDC, 0x86, 0x0E, 0xFF, 0xDC, + 0x85, 0x0E, 0xFF, 0xDC, 0x85, 0x0E, 0xFF, 0xDC, 0x85, 0x0E, 0xFF, 0xDC, 0x85, 0x0E, 0xFF, 0xE6, 0xC0, 0x5F, 0xFF, + 0xE8, 0xBE, 0x57, 0xFF, 0xE9, 0xBB, 0x4F, 0xFF, 0xE6, 0xBA, 0x4E, 0xFF, 0xE3, 0xB9, 0x4D, 0xFF, 0xED, 0xB6, 0x50, + 0xFF, 0xE7, 0xAE, 0x2D, 0xFF, 0xE6, 0xAC, 0x2E, 0xFF, 0xE4, 0xAB, 0x2E, 0xFF, 0xE6, 0xAC, 0x2E, 0xFF, 0xE8, 0xAD, + 0x2E, 0xFF, 0xE7, 0xAB, 0x2D, 0xFF, 0xE5, 0xAA, 0x2C, 0xFF, 0xFF, 0xB2, 0x15, 0xFF, 0x10, 0x42, 0xEB, 0xFF, 0x16, + 0x4F, 0xF1, 0xFF, 0x1C, 0x5C, 0xF7, 0xFF, 0x23, 0x71, 0xF8, 0xFF, 0x29, 0x85, 0xF9, 0xFF, 0x2D, 0x88, 0xF6, 0xFF, + 0x30, 0x8B, 0xF3, 0xFF, 0x31, 0x85, 0xF4, 0xFF, 0x33, 0x7F, 0xF4, 0xFF, 0x35, 0x85, 0xF6, 0xFF, 0x37, 0x8B, 0xF9, + 0xFF, 0x38, 0x8D, 0xF8, 0xFF, 0x3A, 0x90, 0xF7, 0xFF, 0x37, 0x8B, 0xF8, 0xFF, 0x35, 0x86, 0xF8, 0xFF, 0x35, 0x7E, + 0xF7, 0xFF, 0x35, 0x75, 0xF6, 0xFF, 0x33, 0x6D, 0xF7, 0xFF, 0x31, 0x64, 0xF7, 0xFF, 0x31, 0x5E, 0xF8, 0xFF, 0x30, + 0x57, 0xF8, 0xFF, 0x25, 0x51, 0xFF, 0xFF, 0x36, 0x51, 0xF5, 0xFF, 0xFD, 0xA4, 0x03, 0xFF, 0xE1, 0x9A, 0x1E, 0xFF, + 0xE3, 0x98, 0x1E, 0xFF, 0xE5, 0x96, 0x1E, 0xFF, 0xE2, 0x96, 0x1C, 0xFF, 0xDF, 0x97, 0x19, 0xFF, 0xE1, 0x96, 0x18, + 0xFF, 0xE3, 0x95, 0x17, 0xFF, 0xE2, 0x94, 0x16, 0xFF, 0xE1, 0x93, 0x16, 0xFF, 0xE0, 0x92, 0x16, 0xFF, 0xE0, 0x91, + 0x15, 0xFF, 0xE2, 0x92, 0x16, 0xFF, 0xE4, 0x93, 0x16, 0xFF, 0xE1, 0x90, 0x12, 0xFF, 0xDF, 0x8E, 0x0F, 0xFF, 0xE1, + 0x8D, 0x14, 0xFF, 0xE3, 0x8D, 0x18, 0xFF, 0xE0, 0x8C, 0x13, 0xFF, 0xDE, 0x8B, 0x0F, 0xFF, 0xDD, 0x89, 0x0F, 0xFF, + 0xDD, 0x88, 0x0E, 0xFF, 0xDD, 0x87, 0x0E, 0xFF, 0xDC, 0x86, 0x0E, 0xFF, 0xDC, 0x86, 0x0E, 0xFF, 0xDC, 0x86, 0x0E, + 0xFF, 0xDC, 0x86, 0x0E, 0xFF, 0xED, 0xB6, 0x3C, 0xFF, 0xEE, 0xB3, 0x35, 0xFF, 0xEF, 0xB1, 0x2F, 0xFF, 0xED, 0xB1, + 0x2F, 0xFF, 0xEC, 0xB0, 0x2F, 0xFF, 0xEE, 0xB0, 0x38, 0xFF, 0xE9, 0xAE, 0x2D, 0xFF, 0xE7, 0xAD, 0x2F, 0xFF, 0xE6, + 0xAD, 0x30, 0xFF, 0xE8, 0xAE, 0x2F, 0xFF, 0xEA, 0xB0, 0x2D, 0xFF, 0xEC, 0xAD, 0x30, 0xFF, 0xEE, 0xAF, 0x28, 0xFF, + 0xC8, 0xA9, 0x2F, 0xFF, 0x04, 0x3D, 0xFF, 0xFF, 0x19, 0x50, 0xFA, 0xFF, 0x21, 0x5F, 0xF8, 0xFF, 0x28, 0x73, 0xF7, + 0xFF, 0x2F, 0x87, 0xF7, 0xFF, 0x37, 0x95, 0xFA, 0xFF, 0x37, 0x9B, 0xF5, 0xFF, 0x3A, 0x96, 0xF5, 0xFF, 0x3D, 0x92, + 0xF5, 0xFF, 0x3F, 0x94, 0xF7, 0xFF, 0x41, 0x96, 0xF9, 0xFF, 0x43, 0x99, 0xF9, 0xFF, 0x46, 0x9D, 0xF9, 0xFF, 0x44, + 0x98, 0xF8, 0xFF, 0x43, 0x94, 0xF7, 0xFF, 0x42, 0x8D, 0xF8, 0xFF, 0x41, 0x86, 0xF9, 0xFF, 0x3F, 0x7D, 0xF9, 0xFF, + 0x3C, 0x73, 0xF9, 0xFF, 0x38, 0x70, 0xF7, 0xFF, 0x35, 0x6C, 0xF4, 0xFF, 0x21, 0x60, 0xFF, 0xFF, 0x62, 0x6C, 0xBE, + 0xFF, 0xEF, 0x9D, 0x12, 0xFF, 0xE8, 0x9A, 0x21, 0xFF, 0xED, 0x99, 0x1C, 0xFF, 0xE3, 0x9B, 0x17, 0xFF, 0xF0, 0x98, + 0x13, 0xFF, 0xE0, 0x94, 0x1B, 0xFF, 0xE1, 0x96, 0x1A, 0xFF, 0xE3, 0x97, 0x19, 0xFF, 0xE4, 0x96, 0x18, 0xFF, 0xE5, + 0x95, 0x17, 0xFF, 0xE3, 0x94, 0x18, 0xFF, 0xE2, 0x93, 0x19, 0xFF, 0xE0, 0x91, 0x16, 0xFF, 0xDE, 0x90, 0x14, 0xFF, + 0xE1, 0x91, 0x15, 0xFF, 0xE5, 0x92, 0x16, 0xFF, 0xE3, 0x90, 0x14, 0xFF, 0xE2, 0x8D, 0x11, 0xFF, 0xE2, 0x8D, 0x10, + 0xFF, 0xE3, 0x8D, 0x0F, 0xFF, 0xDE, 0x8A, 0x10, 0xFF, 0xD8, 0x88, 0x11, 0xFF, 0xE1, 0x87, 0x0E, 0xFF, 0xDC, 0x89, + 0x0B, 0xFF, 0xE0, 0x85, 0x10, 0xFF, 0xE4, 0x87, 0x09, 0xFF, 0xE4, 0x87, 0x09, 0xFF, 0xE8, 0xB5, 0x3F, 0xFF, 0xE9, + 0xB3, 0x3B, 0xFF, 0xEA, 0xB2, 0x36, 0xFF, 0xE9, 0xB1, 0x37, 0xFF, 0xE8, 0xB1, 0x37, 0xFF, 0xE9, 0xAF, 0x32, 0xFF, + 0xEA, 0xAE, 0x2D, 0xFF, 0xE9, 0xAE, 0x30, 0xFF, 0xE8, 0xAF, 0x32, 0xFF, 0xEA, 0xB1, 0x30, 0xFF, 0xEC, 0xB4, 0x2D, + 0xFF, 0xF1, 0xAE, 0x34, 0xFF, 0xF6, 0xB4, 0x24, 0xFF, 0x86, 0x7E, 0x8D, 0xFF, 0x00, 0x4E, 0xF6, 0xFF, 0x1D, 0x5C, + 0xEC, 0xFF, 0x25, 0x63, 0xF9, 0xFF, 0x2D, 0x76, 0xF7, 0xFF, 0x35, 0x89, 0xF4, 0xFF, 0x41, 0xA2, 0xFD, 0xFF, 0x3E, + 0xAB, 0xF6, 0xFF, 0x43, 0xA8, 0xF6, 0xFF, 0x47, 0xA4, 0xF7, 0xFF, 0x4A, 0xA3, 0xF8, 0xFF, 0x4C, 0xA1, 0xFA, 0xFF, + 0x4E, 0xA5, 0xFA, 0xFF, 0x51, 0xAA, 0xFB, 0xFF, 0x52, 0xA6, 0xF9, 0xFF, 0x52, 0xA2, 0xF7, 0xFF, 0x4F, 0x9C, 0xFA, + 0xFF, 0x4D, 0x97, 0xFD, 0xFF, 0x4A, 0x8D, 0xFC, 0xFF, 0x47, 0x83, 0xFB, 0xFF, 0x40, 0x82, 0xF6, 0xFF, 0x39, 0x82, + 0xF1, 0xFF, 0x2B, 0x72, 0xF4, 0xFF, 0xAB, 0x8C, 0x71, 0xFF, 0xF0, 0x99, 0x16, 0xFF, 0xEF, 0x99, 0x25, 0xFF, 0xE8, + 0x97, 0x25, 0xFF, 0xC5, 0x9A, 0x26, 0xFF, 0xF0, 0x96, 0x16, 0xFF, 0xE2, 0x91, 0x1C, 0xFF, 0xE2, 0x96, 0x1B, 0xFF, + 0xE2, 0x9A, 0x1B, 0xFF, 0xE5, 0x99, 0x19, 0xFF, 0xE8, 0x98, 0x18, 0xFF, 0xE6, 0x96, 0x1A, 0xFF, 0xE4, 0x95, 0x1C, + 0xFF, 0xDF, 0x91, 0x17, 0xFF, 0xD9, 0x8D, 0x13, 0xFF, 0xE2, 0x92, 0x18, 0xFF, 0xEA, 0x97, 0x1E, 0xFF, 0xE5, 0x92, + 0x14, 0xFF, 0xE1, 0x8D, 0x0B, 0xFF, 0xE5, 0x8E, 0x0D, 0xFF, 0xE9, 0x8F, 0x10, 0xFF, 0xDE, 0x8B, 0x12, 0xFF, 0xD4, + 0x88, 0x14, 0xFF, 0xE6, 0x87, 0x0E, 0xFF, 0xDC, 0x8C, 0x08, 0xFF, 0xE4, 0x84, 0x11, 0xFF, 0xEC, 0x88, 0x03, 0xFF, + 0xEC, 0x88, 0x03, 0xFF, 0xEA, 0xB6, 0x3D, 0xFF, 0xEA, 0xB5, 0x3A, 0xFF, 0xEB, 0xB4, 0x38, 0xFF, 0xEB, 0xB3, 0x37, + 0xFF, 0xEA, 0xB3, 0x37, 0xFF, 0xEB, 0xB2, 0x34, 0xFF, 0xEB, 0xB1, 0x32, 0xFF, 0xEB, 0xB1, 0x33, 0xFF, 0xEA, 0xB0, + 0x34, 0xFF, 0xE9, 0xB3, 0x32, 0xFF, 0xE8, 0xB5, 0x2F, 0xFF, 0xF0, 0xB0, 0x34, 0xFF, 0xF8, 0xB6, 0x22, 0xFF, 0x44, + 0x60, 0xC5, 0xFF, 0x0B, 0x53, 0xF9, 0xFF, 0x21, 0x63, 0xF2, 0xFF, 0x29, 0x6F, 0xF6, 0xFF, 0x2F, 0x7D, 0xF6, 0xFF, + 0x35, 0x8A, 0xF7, 0xFF, 0x41, 0xA1, 0xFA, 0xFF, 0x45, 0xAF, 0xF6, 0xFF, 0x4F, 0xB4, 0xFA, 0xFF, 0x50, 0xB0, 0xF6, + 0xFF, 0x53, 0xAE, 0xF8, 0xFF, 0x56, 0xAC, 0xFA, 0xFF, 0x59, 0xB2, 0xFC, 0xFF, 0x5D, 0xB7, 0xFD, 0xFF, 0x5F, 0xB3, + 0xFA, 0xFF, 0x61, 0xAF, 0xF6, 0xFF, 0x5D, 0xAC, 0xF9, 0xFF, 0x59, 0xA9, 0xFD, 0xFF, 0x55, 0x9F, 0xFB, 0xFF, 0x50, + 0x94, 0xF8, 0xFF, 0x4A, 0x91, 0xF7, 0xFF, 0x44, 0x8D, 0xF5, 0xFF, 0x22, 0x7D, 0xFF, 0xFF, 0xEF, 0xA5, 0x1A, 0xFF, + 0xF3, 0x9E, 0x12, 0xFF, 0xF1, 0x96, 0x28, 0xFF, 0xB0, 0x9F, 0x22, 0xFF, 0x00, 0x96, 0x6C, 0xFF, 0x82, 0x9B, 0x3B, + 0xFF, 0xF8, 0x9D, 0x16, 0xFF, 0xF4, 0x9B, 0x15, 0xFF, 0xE2, 0x9C, 0x14, 0xFF, 0xE4, 0x99, 0x15, 0xFF, 0xE6, 0x96, + 0x17, 0xFF, 0xE5, 0x95, 0x18, 0xFF, 0xE4, 0x93, 0x1A, 0xFF, 0xE2, 0x93, 0x18, 0xFF, 0xE0, 0x92, 0x16, 0xFF, 0xE6, + 0x98, 0x1C, 0xFF, 0xE4, 0x95, 0x19, 0xFF, 0xE4, 0x92, 0x16, 0xFF, 0xE5, 0x8F, 0x12, 0xFF, 0xEB, 0x8C, 0x12, 0xFF, + 0xE3, 0x8B, 0x12, 0xFF, 0xE3, 0x87, 0x00, 0xFF, 0xF4, 0x7B, 0x00, 0xFF, 0xD3, 0x86, 0x1A, 0xFF, 0xF0, 0x8C, 0x0C, + 0xFF, 0xE2, 0x8E, 0x00, 0xFF, 0xEA, 0x84, 0x0D, 0xFF, 0xF1, 0x86, 0x07, 0xFF, 0xEC, 0xB7, 0x3B, 0xFF, 0xEC, 0xB6, + 0x3A, 0xFF, 0xEC, 0xB6, 0x39, 0xFF, 0xEC, 0xB5, 0x38, 0xFF, 0xED, 0xB5, 0x37, 0xFF, 0xEC, 0xB4, 0x37, 0xFF, 0xEC, + 0xB4, 0x37, 0xFF, 0xEC, 0xB3, 0x36, 0xFF, 0xEC, 0xB2, 0x36, 0xFF, 0xE8, 0xB4, 0x33, 0xFF, 0xE4, 0xB5, 0x31, 0xFF, + 0xEF, 0xB1, 0x34, 0xFF, 0xF9, 0xB8, 0x21, 0xFF, 0x02, 0x41, 0xFD, 0xFF, 0x1E, 0x58, 0xFC, 0xFF, 0x25, 0x6A, 0xF8, + 0xFF, 0x2C, 0x7C, 0xF3, 0xFF, 0x31, 0x84, 0xF6, 0xFF, 0x35, 0x8B, 0xF9, 0xFF, 0x41, 0xA0, 0xF7, 0xFF, 0x4C, 0xB4, + 0xF6, 0xFF, 0x5B, 0xC0, 0xFE, 0xFF, 0x59, 0xBC, 0xF6, 0xFF, 0x5D, 0xBA, 0xF8, 0xFF, 0x60, 0xB7, 0xFA, 0xFF, 0x64, + 0xBE, 0xFD, 0xFF, 0x69, 0xC4, 0xFF, 0xFF, 0x6C, 0xC0, 0xFA, 0xFF, 0x6F, 0xBD, 0xF5, 0xFF, 0x6A, 0xBC, 0xF9, 0xFF, + 0x65, 0xBB, 0xFD, 0xFF, 0x60, 0xB1, 0xFA, 0xFF, 0x5A, 0xA6, 0xF6, 0xFF, 0x54, 0x9F, 0xF8, 0xFF, 0x4F, 0x98, 0xFA, + 0xFF, 0x6E, 0x94, 0xDF, 0xFF, 0xFB, 0xA6, 0x07, 0xFF, 0xDA, 0x9C, 0x24, 0xFF, 0xF2, 0x9F, 0x14, 0xFF, 0x71, 0xA1, + 0x4A, 0xFF, 0x0D, 0xA9, 0x68, 0xFF, 0x06, 0xA3, 0x61, 0xFF, 0x1B, 0x98, 0x5A, 0xFF, 0x9B, 0x96, 0x33, 0xFF, 0xFE, + 0x99, 0x0D, 0xFF, 0xF1, 0x96, 0x11, 0xFF, 0xE4, 0x94, 0x16, 0xFF, 0xE4, 0x93, 0x17, 0xFF, 0xE4, 0x91, 0x18, 0xFF, + 0xE5, 0x94, 0x19, 0xFF, 0xE6, 0x98, 0x1A, 0xFF, 0xEA, 0x9D, 0x1F, 0xFF, 0xDE, 0x93, 0x15, 0xFF, 0xE3, 0x92, 0x17, + 0xFF, 0xE8, 0x91, 0x1A, 0xFF, 0xEB, 0x94, 0x1F, 0xFF, 0xD1, 0x9D, 0x25, 0xFF, 0x72, 0xF7, 0xD0, 0xFF, 0x95, 0xF2, + 0xC1, 0xFF, 0xF0, 0x83, 0x00, 0xFF, 0xA0, 0x81, 0x17, 0xFF, 0x2E, 0x7E, 0x3B, 0xFF, 0xCB, 0x87, 0x16, 0xFF, 0xDA, + 0x8A, 0x0B, 0xFF, 0xEC, 0xB8, 0x3D, 0xFF, 0xED, 0xB8, 0x3C, 0xFF, 0xED, 0xB7, 0x3B, 0xFF, 0xED, 0xB7, 0x3A, 0xFF, + 0xED, 0xB6, 0x39, 0xFF, 0xED, 0xB6, 0x39, 0xFF, 0xED, 0xB6, 0x39, 0xFF, 0xED, 0xB6, 0x39, 0xFF, 0xED, 0xB6, 0x39, + 0xFF, 0xEC, 0xB4, 0x37, 0xFF, 0xEB, 0xB2, 0x34, 0xFF, 0xF2, 0xAB, 0x34, 0xFF, 0xB3, 0x95, 0x6D, 0xFF, 0x00, 0x46, + 0xFF, 0xFF, 0x20, 0x64, 0xF7, 0xFF, 0x28, 0x73, 0xF6, 0xFF, 0x30, 0x81, 0xF5, 0xFF, 0x37, 0x8B, 0xF6, 0xFF, 0x3D, + 0x94, 0xF8, 0xFF, 0x48, 0xA6, 0xF8, 0xFF, 0x53, 0xB7, 0xF7, 0xFF, 0x60, 0xC2, 0xFB, 0xFF, 0x65, 0xC4, 0xF7, 0xFF, + 0x69, 0xC3, 0xF9, 0xFF, 0x6D, 0xC2, 0xFA, 0xFF, 0x72, 0xC6, 0xFA, 0xFF, 0x77, 0xCB, 0xFA, 0xFF, 0x7A, 0xCB, 0xFB, + 0xFF, 0x7D, 0xCB, 0xFC, 0xFF, 0x7A, 0xC8, 0xFA, 0xFF, 0x77, 0xC5, 0xF8, 0xFF, 0x72, 0xBC, 0xF9, 0xFF, 0x6C, 0xB4, + 0xFA, 0xFF, 0x68, 0xB0, 0xF6, 0xFF, 0x56, 0xAA, 0xFD, 0xFF, 0xA5, 0xA0, 0x93, 0xFF, 0xF3, 0xA1, 0x13, 0xFF, 0xEF, + 0x9C, 0x21, 0xFF, 0xFF, 0x9D, 0x19, 0xFF, 0x23, 0xC1, 0x71, 0xFF, 0x25, 0xB7, 0x79, 0xFF, 0x1D, 0xB2, 0x71, 0xFF, + 0x23, 0xAA, 0x6A, 0xFF, 0x25, 0xA0, 0x66, 0xFF, 0x18, 0x9A, 0x63, 0xFF, 0x72, 0x9C, 0x41, 0xFF, 0xCB, 0x9F, 0x1E, + 0xFF, 0xFF, 0x93, 0x18, 0xFF, 0xF1, 0x98, 0x13, 0xFF, 0xF4, 0x9C, 0x18, 0xFF, 0xF7, 0xA0, 0x1D, 0xFF, 0xFF, 0x9C, + 0x1B, 0xFF, 0xF6, 0x93, 0x10, 0xFF, 0xF1, 0x93, 0x11, 0xFF, 0xEC, 0x93, 0x13, 0xFF, 0xFF, 0x83, 0x00, 0xFF, 0xA0, + 0xCB, 0x72, 0xFF, 0x81, 0xF9, 0xCB, 0xFF, 0xAC, 0xFF, 0xD0, 0xFF, 0x45, 0xA0, 0x78, 0xFF, 0x00, 0x77, 0x33, 0xFF, + 0x02, 0x7C, 0x3A, 0xFF, 0xE2, 0x8C, 0x0D, 0xFF, 0xDB, 0x8E, 0x0D, 0xFF, 0xED, 0xBA, 0x3E, 0xFF, 0xED, 0xB9, 0x3D, + 0xFF, 0xED, 0xB9, 0x3C, 0xFF, 0xED, 0xB8, 0x3B, 0xFF, 0xED, 0xB8, 0x3A, 0xFF, 0xED, 0xB8, 0x3B, 0xFF, 0xED, 0xB8, + 0x3B, 0xFF, 0xEE, 0xB8, 0x3C, 0xFF, 0xEE, 0xB9, 0x3C, 0xFF, 0xF0, 0xB4, 0x3A, 0xFF, 0xF2, 0xAE, 0x37, 0xFF, 0xFE, + 0xB3, 0x32, 0xFF, 0x7C, 0x8E, 0xB3, 0xFF, 0x06, 0x58, 0xFF, 0xFF, 0x22, 0x71, 0xF3, 0xFF, 0x2B, 0x7C, 0xF4, 0xFF, + 0x34, 0x86, 0xF6, 0xFF, 0x3D, 0x92, 0xF7, 0xFF, 0x45, 0x9D, 0xF8, 0xFF, 0x4F, 0xAC, 0xF8, 0xFF, 0x5A, 0xBB, 0xF8, + 0xFF, 0x65, 0xC4, 0xF9, 0xFF, 0x70, 0xCC, 0xF9, 0xFF, 0x75, 0xCC, 0xFA, 0xFF, 0x7A, 0xCC, 0xFA, 0xFF, 0x80, 0xCF, + 0xF7, 0xFF, 0x85, 0xD2, 0xF4, 0xFF, 0x89, 0xD5, 0xFB, 0xFF, 0x8C, 0xD9, 0xFF, 0xFF, 0x8B, 0xD3, 0xFA, 0xFF, 0x89, + 0xCE, 0xF2, 0xFF, 0x84, 0xC8, 0xF8, 0xFF, 0x7F, 0xC1, 0xFE, 0xFF, 0x7C, 0xC1, 0xF4, 0xFF, 0x5E, 0xBC, 0xFF, 0xFF, + 0xDB, 0xAB, 0x47, 0xFF, 0xEA, 0x9C, 0x1E, 0xFF, 0xE8, 0xA2, 0x1D, 0xFF, 0xE5, 0xA7, 0x1D, 0xFF, 0x1B, 0xD3, 0x98, + 0xFF, 0x21, 0xCB, 0x8A, 0xFF, 0x26, 0xC3, 0x82, 0xFF, 0x2C, 0xBB, 0x7A, 0xFF, 0x28, 0xB4, 0x75, 0xFF, 0x25, 0xAD, + 0x70, 0xFF, 0x16, 0xAB, 0x6D, 0xFF, 0x08, 0xA9, 0x6A, 0xFF, 0x11, 0xA9, 0x5E, 0xFF, 0x53, 0x9E, 0x51, 0xFF, 0x6D, + 0x9B, 0x47, 0xFF, 0x87, 0x97, 0x3E, 0xFF, 0x91, 0x95, 0x3B, 0xFF, 0x80, 0x98, 0x38, 0xFF, 0x63, 0x96, 0x44, 0xFF, + 0x45, 0x94, 0x4F, 0xFF, 0x3C, 0xB4, 0x82, 0xFF, 0x1B, 0x84, 0x4F, 0xFF, 0x87, 0xE0, 0xAF, 0xFF, 0x82, 0xCC, 0x9E, + 0xFF, 0x11, 0x7F, 0x35, 0xFF, 0x1B, 0x82, 0x42, 0xFF, 0x3B, 0x84, 0x32, 0xFF, 0xF9, 0x92, 0x04, 0xFF, 0xDC, 0x92, + 0x0F, 0xFF, 0xEE, 0xBC, 0x40, 0xFF, 0xED, 0xBB, 0x3F, 0xFF, 0xED, 0xBA, 0x3E, 0xFF, 0xED, 0xBA, 0x3D, 0xFF, 0xEC, + 0xB9, 0x3C, 0xFF, 0xEC, 0xB9, 0x3C, 0xFF, 0xEC, 0xB8, 0x3C, 0xFF, 0xEC, 0xB8, 0x3C, 0xFF, 0xEB, 0xB8, 0x3C, 0xFF, + 0xF0, 0xB3, 0x3F, 0xFF, 0xF4, 0xAF, 0x42, 0xFF, 0xE8, 0xBA, 0x0D, 0xFF, 0x96, 0xB8, 0xFF, 0xFF, 0x4C, 0x81, 0xF6, + 0xFF, 0x22, 0x75, 0xF5, 0xFF, 0x2D, 0x80, 0xF6, 0xFF, 0x38, 0x8B, 0xF7, 0xFF, 0x42, 0x99, 0xF7, 0xFF, 0x4D, 0xA6, + 0xF7, 0xFF, 0x56, 0xB2, 0xF8, 0xFF, 0x5F, 0xBD, 0xF9, 0xFF, 0x6D, 0xC8, 0xF9, 0xFF, 0x7A, 0xD4, 0xFA, 0xFF, 0x81, + 0xD5, 0xFA, 0xFF, 0x88, 0xD7, 0xF9, 0xFF, 0x8D, 0xD8, 0xFA, 0xFF, 0x92, 0xDA, 0xFB, 0xFF, 0xA1, 0xE4, 0xF9, 0xFF, + 0x91, 0xD6, 0xFE, 0xFF, 0x9F, 0xDE, 0xFA, 0xFF, 0x97, 0xDB, 0xF8, 0xFF, 0x93, 0xD5, 0xF9, 0xFF, 0x8F, 0xCF, 0xFB, + 0xFF, 0x85, 0xD1, 0xFF, 0xFF, 0x78, 0xC6, 0xFF, 0xFF, 0xFC, 0x9A, 0x00, 0xFF, 0xF1, 0xA8, 0x26, 0xFF, 0xF8, 0xA4, + 0x1F, 0xFF, 0xA5, 0xBD, 0x53, 0xFF, 0x30, 0xDA, 0xA4, 0xFF, 0x37, 0xD5, 0x9D, 0xFF, 0x3A, 0xD0, 0x97, 0xFF, 0x3D, + 0xCA, 0x90, 0xFF, 0x39, 0xC5, 0x8A, 0xFF, 0x35, 0xBF, 0x84, 0xFF, 0x30, 0xBD, 0x7C, 0xFF, 0x2C, 0xBC, 0x74, 0xFF, + 0x1B, 0xB8, 0x75, 0xFF, 0x27, 0xAF, 0x77, 0xFF, 0x25, 0xAB, 0x72, 0xFF, 0x23, 0xA7, 0x6D, 0xFF, 0x28, 0xA3, 0x6A, + 0xFF, 0x1E, 0xA2, 0x68, 0xFF, 0x19, 0x95, 0x57, 0xFF, 0x45, 0xB7, 0x77, 0xFF, 0x81, 0xF0, 0xBA, 0xFF, 0x4C, 0xAC, + 0x72, 0xFF, 0x14, 0x7B, 0x41, 0xFF, 0x1D, 0x8A, 0x4F, 0xFF, 0x1C, 0x86, 0x42, 0xFF, 0x14, 0x86, 0x49, 0xFF, 0x8B, + 0x86, 0x16, 0xFF, 0xF5, 0x90, 0x0A, 0xFF, 0xE7, 0x8D, 0x15, 0xFF, 0xEF, 0xBE, 0x41, 0xFF, 0xEE, 0xBD, 0x40, 0xFF, + 0xED, 0xBC, 0x3F, 0xFF, 0xED, 0xBB, 0x3E, 0xFF, 0xEC, 0xBA, 0x3D, 0xFF, 0xEB, 0xBA, 0x3D, 0xFF, 0xEA, 0xB9, 0x3C, + 0xFF, 0xE9, 0xB8, 0x3C, 0xFF, 0xE8, 0xB7, 0x3B, 0xFF, 0xF0, 0xB9, 0x39, 0xFF, 0xF7, 0xBA, 0x37, 0xFF, 0xDC, 0xB5, + 0x50, 0xFF, 0x44, 0x96, 0xFF, 0xFF, 0x9C, 0xC4, 0xFE, 0xFF, 0x23, 0x79, 0xF7, 0xFF, 0x30, 0x85, 0xF8, 0xFF, 0x3C, + 0x91, 0xF8, 0xFF, 0x48, 0xA0, 0xF8, 0xFF, 0x55, 0xAF, 0xF7, 0xFF, 0x5D, 0xB7, 0xF8, 0xFF, 0x65, 0xBF, 0xF9, 0xFF, + 0x75, 0xCD, 0xFA, 0xFF, 0x85, 0xDB, 0xFB, 0xFF, 0x8D, 0xDE, 0xFA, 0xFF, 0x95, 0xE1, 0xF9, 0xFF, 0x9A, 0xE1, 0xFD, + 0xFF, 0xA0, 0xE2, 0xFF, 0xFF, 0xA3, 0xE8, 0xFA, 0xFF, 0x6B, 0xBD, 0xFF, 0xFF, 0x9E, 0xDE, 0xFC, 0xFF, 0xA6, 0xE8, + 0xFF, 0xFF, 0xA3, 0xE3, 0xFB, 0xFF, 0xA0, 0xDE, 0xF7, 0xFF, 0x99, 0xD7, 0xFD, 0xFF, 0xAB, 0xBD, 0xB5, 0xFF, 0xF0, + 0x9F, 0x11, 0xFF, 0xE8, 0xA3, 0x1D, 0xFF, 0xFF, 0x9E, 0x19, 0xFF, 0x65, 0xD4, 0x89, 0xFF, 0x45, 0xE1, 0xB0, 0xFF, + 0x4D, 0xDF, 0xB0, 0xFF, 0x4D, 0xDC, 0xAB, 0xFF, 0x4D, 0xD8, 0xA7, 0xFF, 0x49, 0xD5, 0xA0, 0xFF, 0x44, 0xD2, 0x99, + 0xFF, 0x3C, 0xCD, 0x97, 0xFF, 0x34, 0xC9, 0x94, 0xFF, 0x34, 0xC4, 0x8D, 0xFF, 0x33, 0xC0, 0x86, 0xFF, 0x32, 0xBC, + 0x7A, 0xFF, 0x31, 0xB7, 0x6E, 0xFF, 0x2F, 0xB2, 0x6D, 0xFF, 0x2E, 0xAE, 0x6B, 0xFF, 0x3F, 0xB9, 0x7D, 0xFF, 0x30, + 0xA5, 0x6F, 0xFF, 0x4E, 0xB5, 0x7B, 0xFF, 0x20, 0x9A, 0x56, 0xFF, 0x2A, 0x9F, 0x5B, 0xFF, 0x24, 0x93, 0x50, 0xFF, + 0x65, 0xB9, 0x80, 0xFF, 0x1C, 0x99, 0x5F, 0xFF, 0xE2, 0x8F, 0x03, 0xFF, 0xF2, 0x8E, 0x10, 0xFF, 0xF2, 0x88, 0x1B, + 0xFF, 0xEF, 0xBF, 0x43, 0xFF, 0xEE, 0xBE, 0x42, 0xFF, 0xEE, 0xBD, 0x41, 0xFF, 0xEE, 0xBD, 0x40, 0xFF, 0xED, 0xBC, + 0x3F, 0xFF, 0xEC, 0xBB, 0x3F, 0xFF, 0xEB, 0xB9, 0x3F, 0xFF, 0xEC, 0xB9, 0x3D, 0xFF, 0xEE, 0xB8, 0x3C, 0xFF, 0xEB, + 0xB8, 0x37, 0xFF, 0xF6, 0xBC, 0x26, 0xFF, 0x8F, 0x9B, 0x94, 0xFF, 0x37, 0x96, 0xFB, 0xFF, 0x7C, 0xBB, 0xF9, 0xFF, + 0x85, 0xB5, 0xF8, 0xFF, 0x49, 0x99, 0xF6, 0xFF, 0x42, 0x9B, 0xF5, 0xFF, 0x4E, 0xA6, 0xF6, 0xFF, 0x59, 0xB2, 0xF7, + 0xFF, 0x65, 0xBC, 0xF8, 0xFF, 0x72, 0xC6, 0xF9, 0xFF, 0x7F, 0xD3, 0xF9, 0xFF, 0x8D, 0xE0, 0xFA, 0xFF, 0x97, 0xE5, + 0xF9, 0xFF, 0xA1, 0xEB, 0xF8, 0xFF, 0xA6, 0xEA, 0xFE, 0xFF, 0xAA, 0xEA, 0xFF, 0xFF, 0xA8, 0xEE, 0xFC, 0xFF, 0x62, + 0xBA, 0xF9, 0xFF, 0x98, 0xDC, 0xFA, 0xFF, 0xB9, 0xF3, 0xFE, 0xFF, 0xB2, 0xEC, 0xFB, 0xFF, 0xAB, 0xE5, 0xF7, 0xFF, + 0xA2, 0xE4, 0xFE, 0xFF, 0xD1, 0xB0, 0x64, 0xFF, 0xF0, 0x9F, 0x19, 0xFF, 0xE8, 0x9E, 0x26, 0xFF, 0xF2, 0x98, 0x03, + 0xFF, 0x50, 0xEF, 0xE3, 0xFF, 0x57, 0xEE, 0xD5, 0xFF, 0x64, 0xE3, 0xBF, 0xFF, 0x64, 0xE1, 0xBC, 0xFF, 0x64, 0xDF, + 0xB9, 0xFF, 0x5D, 0xDD, 0xB4, 0xFF, 0x56, 0xDB, 0xB0, 0xFF, 0x4E, 0xD7, 0xA9, 0xFF, 0x46, 0xD3, 0xA2, 0xFF, 0x42, + 0xD0, 0x9B, 0xFF, 0x3F, 0xCD, 0x93, 0xFF, 0x3D, 0xC9, 0x8B, 0xFF, 0x3C, 0xC5, 0x84, 0xFF, 0x39, 0xC1, 0x80, 0xFF, + 0x36, 0xBC, 0x7D, 0xFF, 0x45, 0xC7, 0x8A, 0xFF, 0x44, 0xC1, 0x88, 0xFF, 0x2B, 0xA0, 0x62, 0xFF, 0x2B, 0xA9, 0x64, + 0xFF, 0x2D, 0xA3, 0x5E, 0xFF, 0x26, 0x95, 0x4F, 0xFF, 0x98, 0xCE, 0xA4, 0xFF, 0xDC, 0xEA, 0xD8, 0xFF, 0xFF, 0xDC, + 0xB9, 0xFF, 0xF3, 0x9D, 0x38, 0xFF, 0xD3, 0x8F, 0x00, 0xFF, 0xEF, 0xC1, 0x45, 0xFF, 0xEF, 0xC0, 0x44, 0xFF, 0xEF, + 0xBF, 0x43, 0xFF, 0xEF, 0xBE, 0x41, 0xFF, 0xEF, 0xBD, 0x40, 0xFF, 0xED, 0xBC, 0x41, 0xFF, 0xEB, 0xBA, 0x42, 0xFF, + 0xEF, 0xBA, 0x3F, 0xFF, 0xF3, 0xB9, 0x3C, 0xFF, 0xE6, 0xB8, 0x34, 0xFF, 0xF6, 0xBD, 0x16, 0xFF, 0x4F, 0x7F, 0xD8, + 0xFF, 0x46, 0x90, 0xF7, 0xFF, 0x54, 0xA5, 0xF7, 0xFF, 0xBA, 0xDA, 0xFF, 0xFF, 0x4D, 0xA1, 0xF8, 0xFF, 0x49, 0xA5, + 0xF3, 0xFF, 0x53, 0xAD, 0xF4, 0xFF, 0x5D, 0xB5, 0xF6, 0xFF, 0x6E, 0xC0, 0xF8, 0xFF, 0x7F, 0xCC, 0xFA, 0xFF, 0x8A, + 0xD8, 0xF9, 0xFF, 0x95, 0xE4, 0xF8, 0xFF, 0xA1, 0xEC, 0xF8, 0xFF, 0xAE, 0xF4, 0xF7, 0xFF, 0xB2, 0xF3, 0xFE, 0xFF, + 0xB5, 0xF1, 0xFF, 0xFF, 0xAD, 0xF4, 0xFE, 0xFF, 0x59, 0xB6, 0xF3, 0xFF, 0x92, 0xDA, 0xF8, 0xFF, 0xCC, 0xFF, 0xFE, + 0xFF, 0xC1, 0xF6, 0xFA, 0xFF, 0xB6, 0xED, 0xF7, 0xFF, 0xAB, 0xF1, 0xFF, 0xFF, 0xF7, 0xA4, 0x13, 0xFF, 0xEF, 0xA4, + 0x15, 0xFF, 0xE8, 0xA5, 0x18, 0xFF, 0xCD, 0xB4, 0x56, 0xFF, 0x71, 0xF2, 0xF0, 0xFF, 0x84, 0xEF, 0xD4, 0xFF, 0x7B, + 0xE6, 0xCF, 0xFF, 0x7B, 0xE6, 0xCD, 0xFF, 0x7C, 0xE6, 0xCB, 0xFF, 0x71, 0xE5, 0xC9, 0xFF, 0x67, 0xE5, 0xC6, 0xFF, + 0x5F, 0xE1, 0xBC, 0xFF, 0x57, 0xDD, 0xB1, 0xFF, 0x51, 0xDB, 0xA8, 0xFF, 0x4B, 0xDA, 0xA0, 0xFF, 0x48, 0xD7, 0x9C, + 0xFF, 0x46, 0xD4, 0x99, 0xFF, 0x42, 0xCF, 0x94, 0xFF, 0x3E, 0xCA, 0x8F, 0xFF, 0x3B, 0xC4, 0x88, 0xFF, 0x39, 0xBE, + 0x81, 0xFF, 0x30, 0xB3, 0x72, 0xFF, 0x27, 0xA8, 0x62, 0xFF, 0x27, 0xA0, 0x58, 0xFF, 0x27, 0x97, 0x4E, 0xFF, 0x79, + 0xC4, 0x9F, 0xFF, 0xF7, 0xFB, 0xFF, 0xFF, 0xF4, 0xD2, 0x7F, 0xFF, 0xE1, 0x8E, 0x03, 0xFF, 0xE1, 0x89, 0x0E, 0xFF, + 0xEF, 0xC3, 0x47, 0xFF, 0xEF, 0xC2, 0x46, 0xFF, 0xEF, 0xC0, 0x44, 0xFF, 0xEF, 0xBF, 0x43, 0xFF, 0xF0, 0xBE, 0x41, + 0xFF, 0xEE, 0xBD, 0x42, 0xFF, 0xEC, 0xBC, 0x43, 0xFF, 0xEF, 0xBC, 0x40, 0xFF, 0xF1, 0xBB, 0x3E, 0xFF, 0xFD, 0xC0, + 0x2F, 0xFF, 0xFB, 0xBD, 0x35, 0xFF, 0x00, 0x4B, 0xF5, 0xFF, 0x52, 0x8A, 0xFF, 0xFF, 0x5D, 0xA5, 0xFA, 0xFF, 0x8D, + 0xC4, 0xFC, 0xFF, 0x85, 0xC1, 0xFB, 0xFF, 0x50, 0xAD, 0xF5, 0xFF, 0x5E, 0xB6, 0xF7, 0xFF, 0x6B, 0xBE, 0xF9, 0xFF, + 0x78, 0xC9, 0xFA, 0xFF, 0x85, 0xD4, 0xFB, 0xFF, 0x97, 0xDE, 0xFE, 0xFF, 0xAA, 0xE8, 0xFF, 0xFF, 0xAD, 0xEE, 0xFD, + 0xFF, 0xB1, 0xF4, 0xF9, 0xFF, 0xB9, 0xF5, 0xFC, 0xFF, 0xC2, 0xF6, 0xFE, 0xFF, 0xB2, 0xF0, 0xFB, 0xFF, 0x6E, 0xCB, + 0xF6, 0xFF, 0x91, 0xDE, 0xFB, 0xFF, 0xCA, 0xFC, 0xFC, 0xFF, 0xD0, 0xFB, 0xFF, 0xFF, 0xC8, 0xFC, 0xFF, 0xFF, 0xC7, + 0xE3, 0xCA, 0xFF, 0xF2, 0xA1, 0x15, 0xFF, 0xEE, 0xA3, 0x1D, 0xFF, 0xF1, 0xA1, 0x11, 0xFF, 0xB9, 0xD4, 0x9E, 0xFF, + 0x8B, 0xF1, 0xEA, 0xFF, 0x95, 0xEF, 0xDC, 0xFF, 0x90, 0xEB, 0xD9, 0xFF, 0x92, 0xEB, 0xD9, 0xFF, 0x94, 0xEC, 0xD8, + 0xFF, 0x8B, 0xEB, 0xD6, 0xFF, 0x82, 0xEA, 0xD3, 0xFF, 0x78, 0xE6, 0xC9, 0xFF, 0x6F, 0xE3, 0xBF, 0xFF, 0x68, 0xE2, + 0xB8, 0xFF, 0x61, 0xE2, 0xB1, 0xFF, 0x5D, 0xE0, 0xAE, 0xFF, 0x5A, 0xDE, 0xAC, 0xFF, 0x51, 0xD9, 0xA2, 0xFF, 0x48, + 0xD3, 0x98, 0xFF, 0x41, 0xCB, 0x8E, 0xFF, 0x39, 0xC3, 0x83, 0xFF, 0x32, 0xB7, 0x74, 0xFF, 0x2C, 0xAC, 0x66, 0xFF, + 0x29, 0xA2, 0x5D, 0xFF, 0x26, 0x99, 0x54, 0xFF, 0x21, 0x93, 0x4A, 0xFF, 0xB9, 0x99, 0x23, 0xFF, 0xFE, 0x93, 0x15, + 0xFF, 0xD8, 0x92, 0x09, 0xFF, 0xD8, 0x8F, 0x0F, 0xFF, 0xEF, 0xC4, 0x49, 0xFF, 0xEF, 0xC3, 0x47, 0xFF, 0xF0, 0xC2, + 0x46, 0xFF, 0xF0, 0xC1, 0x44, 0xFF, 0xF1, 0xC0, 0x42, 0xFF, 0xEF, 0xBF, 0x43, 0xFF, 0xED, 0xBE, 0x43, 0xFF, 0xEE, + 0xBE, 0x42, 0xFF, 0xF0, 0xBD, 0x41, 0xFF, 0xF0, 0xBA, 0x37, 0xFF, 0xB7, 0xA1, 0x71, 0xFF, 0x1D, 0x5D, 0xFE, 0xFF, + 0x31, 0x79, 0xF8, 0xFF, 0x51, 0xA1, 0xF5, 0xFF, 0x60, 0xAD, 0xF8, 0xFF, 0xBC, 0xE0, 0xFE, 0xFF, 0x57, 0xB6, 0xF7, + 0xFF, 0x68, 0xBF, 0xF9, 0xFF, 0x79, 0xC8, 0xFC, 0xFF, 0x82, 0xD2, 0xFC, 0xFF, 0x8B, 0xDB, 0xFC, 0xFF, 0x8F, 0xDE, + 0xFB, 0xFF, 0x92, 0xE0, 0xFB, 0xFF, 0xA3, 0xEA, 0xFA, 0xFF, 0xB4, 0xF4, 0xFA, 0xFF, 0xC1, 0xF8, 0xF9, 0xFF, 0xCE, + 0xFB, 0xF8, 0xFF, 0xB6, 0xEB, 0xF9, 0xFF, 0x83, 0xE1, 0xFA, 0xFF, 0x8F, 0xE2, 0xFD, 0xFF, 0xC7, 0xF9, 0xFB, 0xFF, + 0xD7, 0xF8, 0xFC, 0xFF, 0xCA, 0xFC, 0xFE, 0xFF, 0xDC, 0xCD, 0x8B, 0xFF, 0xED, 0x9F, 0x18, 0xFF, 0xED, 0xA3, 0x24, + 0xFF, 0xFA, 0x9D, 0x0A, 0xFF, 0xA5, 0xF5, 0xE7, 0xFF, 0xA5, 0xF1, 0xE4, 0xFF, 0xA5, 0xF0, 0xE4, 0xFF, 0xA6, 0xEF, + 0xE3, 0xFF, 0xA9, 0xF0, 0xE4, 0xFF, 0xAD, 0xF2, 0xE6, 0xFF, 0xA5, 0xF0, 0xE3, 0xFF, 0x9E, 0xEF, 0xE0, 0xFF, 0x92, + 0xEC, 0xD6, 0xFF, 0x87, 0xE9, 0xCD, 0xFF, 0x7F, 0xE9, 0xC7, 0xFF, 0x78, 0xEA, 0xC2, 0xFF, 0x72, 0xEA, 0xC1, 0xFF, + 0x6D, 0xE9, 0xC0, 0xFF, 0x60, 0xE3, 0xB1, 0xFF, 0x53, 0xDD, 0xA2, 0xFF, 0x46, 0xD2, 0x94, 0xFF, 0x3A, 0xC8, 0x86, + 0xFF, 0x35, 0xBC, 0x77, 0xFF, 0x30, 0xB0, 0x69, 0xFF, 0x2B, 0xA5, 0x62, 0xFF, 0x26, 0x9B, 0x5B, 0xFF, 0x09, 0x91, + 0x57, 0xFF, 0xFB, 0x94, 0x09, 0xFF, 0xE5, 0x95, 0x0C, 0xFF, 0xEB, 0x91, 0x0F, 0xFF, 0xEB, 0x91, 0x0F, 0xFF, 0xEF, + 0xC5, 0x4A, 0xFF, 0xF0, 0xC4, 0x48, 0xFF, 0xF0, 0xC3, 0x47, 0xFF, 0xF1, 0xC2, 0x45, 0xFF, 0xF1, 0xC1, 0x43, 0xFF, + 0xF1, 0xC1, 0x41, 0xFF, 0xF1, 0xC1, 0x3F, 0xFF, 0xF0, 0xBE, 0x3F, 0xFF, 0xEF, 0xBC, 0x3F, 0xFF, 0xFD, 0xC2, 0x32, + 0xFF, 0x6E, 0x7F, 0xBD, 0xFF, 0x26, 0x65, 0xFE, 0xFF, 0x34, 0x7B, 0xF5, 0xFF, 0x4C, 0x9A, 0xF5, 0xFF, 0x5C, 0xAB, + 0xF8, 0xFF, 0x9F, 0xD0, 0xFA, 0xFF, 0x83, 0xC6, 0xF7, 0xFF, 0x6A, 0xC1, 0xFD, 0xFF, 0x7E, 0xD1, 0xFD, 0xFF, 0x87, + 0xDB, 0xFB, 0xFF, 0x8F, 0xE5, 0xF9, 0xFF, 0x9A, 0xEC, 0xF8, 0xFF, 0xA5, 0xF4, 0xF7, 0xFF, 0x99, 0xEA, 0xFB, 0xFF, + 0x8E, 0xDF, 0xFF, 0xFF, 0x9F, 0xE2, 0xFB, 0xFF, 0xB1, 0xE6, 0xF7, 0xFF, 0xCC, 0xED, 0xFB, 0xFF, 0xCA, 0xFA, 0xFF, + 0xFF, 0xC6, 0xF2, 0xFF, 0xFF, 0xC2, 0xF0, 0xFC, 0xFF, 0xD2, 0xF5, 0xFE, 0xFF, 0xD3, 0xFC, 0xFF, 0xFF, 0xE6, 0xB5, + 0x4B, 0xFF, 0xED, 0xA4, 0x20, 0xFF, 0xED, 0xA2, 0x1B, 0xFF, 0xE2, 0xAA, 0x3D, 0xFF, 0xAB, 0xF6, 0xEE, 0xFF, 0xB1, + 0xF1, 0xE5, 0xFF, 0xB4, 0xF2, 0xE7, 0xFF, 0xB8, 0xF3, 0xE9, 0xFF, 0xBA, 0xF3, 0xE9, 0xFF, 0xBC, 0xF4, 0xEA, 0xFF, + 0xB5, 0xF3, 0xE8, 0xFF, 0xAF, 0xF2, 0xE5, 0xFF, 0xA8, 0xF0, 0xE0, 0xFF, 0xA1, 0xED, 0xDA, 0xFF, 0x99, 0xEF, 0xD5, + 0xFF, 0x91, 0xF0, 0xD0, 0xFF, 0x82, 0xED, 0xC8, 0xFF, 0x72, 0xEA, 0xC0, 0xFF, 0x61, 0xE3, 0xB0, 0xFF, 0x50, 0xDC, + 0xA0, 0xFF, 0x47, 0xD3, 0x94, 0xFF, 0x3E, 0xCA, 0x88, 0xFF, 0x38, 0xBF, 0x7B, 0xFF, 0x32, 0xB4, 0x6E, 0xFF, 0x2E, + 0xA8, 0x65, 0xFF, 0x1B, 0xA0, 0x5D, 0xFF, 0x48, 0x94, 0x3C, 0xFF, 0xF6, 0x93, 0x0A, 0xFF, 0xEC, 0x94, 0x0D, 0xFF, + 0xF0, 0x92, 0x10, 0xFF, 0xF0, 0x92, 0x10, 0xFF, 0xF0, 0xC5, 0x4B, 0xFF, 0xF0, 0xC4, 0x49, 0xFF, 0xF1, 0xC4, 0x48, + 0xFF, 0xF1, 0xC3, 0x46, 0xFF, 0xF2, 0xC2, 0x44, 0xFF, 0xF4, 0xC3, 0x3F, 0xFF, 0xF6, 0xC4, 0x3A, 0xFF, 0xF3, 0xBF, + 0x3C, 0xFF, 0xEF, 0xBA, 0x3D, 0xFF, 0xFF, 0xCA, 0x2C, 0xFF, 0x24, 0x5D, 0xFF, 0xFF, 0x2E, 0x6D, 0xFE, 0xFF, 0x38, + 0x7D, 0xF2, 0xFF, 0x48, 0x93, 0xF5, 0xFF, 0x57, 0xA9, 0xF7, 0xFF, 0x82, 0xC0, 0xF7, 0xFF, 0xAE, 0xD7, 0xF7, 0xFF, + 0x6C, 0xC2, 0xFF, 0xFF, 0x84, 0xDA, 0xFE, 0xFF, 0x8B, 0xE4, 0xFA, 0xFF, 0x93, 0xEE, 0xF6, 0xFF, 0x9D, 0xED, 0xF8, + 0xFF, 0xA7, 0xEC, 0xF9, 0xFF, 0xB3, 0xF1, 0xF8, 0xFF, 0xC0, 0xF6, 0xF7, 0xFF, 0xC8, 0xF6, 0xFB, 0xFF, 0xD0, 0xF6, + 0xFF, 0xFF, 0xD3, 0xF2, 0xFE, 0xFF, 0xB9, 0xF3, 0xFB, 0xFF, 0xE7, 0xFD, 0xFF, 0xFF, 0xE9, 0xFD, 0xF6, 0xFF, 0xE2, + 0xFC, 0xFC, 0xFF, 0xDC, 0xFC, 0xFF, 0xFF, 0xF1, 0x9D, 0x0B, 0xFF, 0xEC, 0xAA, 0x29, 0xFF, 0xF5, 0xAA, 0x1B, 0xFF, + 0xD9, 0xC7, 0x7F, 0xFF, 0xBA, 0xFE, 0xFD, 0xFF, 0xBD, 0xF2, 0xE7, 0xFF, 0xC3, 0xF4, 0xEB, 0xFF, 0xCA, 0xF6, 0xEE, + 0xFF, 0xCA, 0xF6, 0xEF, 0xFF, 0xCB, 0xF7, 0xEF, 0xFF, 0xC5, 0xF6, 0xED, 0xFF, 0xBF, 0xF5, 0xEB, 0xFF, 0xBE, 0xF3, + 0xE9, 0xFF, 0xBC, 0xF2, 0xE8, 0xFF, 0xB3, 0xF4, 0xE3, 0xFF, 0xAB, 0xF6, 0xDF, 0xFF, 0x91, 0xF1, 0xD0, 0xFF, 0x77, + 0xEC, 0xC1, 0xFF, 0x62, 0xE3, 0xAF, 0xFF, 0x4E, 0xDB, 0x9E, 0xFF, 0x47, 0xD3, 0x94, 0xFF, 0x41, 0xCC, 0x8A, 0xFF, + 0x3B, 0xC2, 0x7F, 0xFF, 0x35, 0xB8, 0x73, 0xFF, 0x30, 0xAC, 0x69, 0xFF, 0x10, 0xA5, 0x60, 0xFF, 0x86, 0x96, 0x22, + 0xFF, 0xF0, 0x91, 0x0A, 0xFF, 0xF2, 0x92, 0x0E, 0xFF, 0xF4, 0x94, 0x11, 0xFF, 0xF4, 0x94, 0x11, 0xFF, 0xF1, 0xC5, + 0x4C, 0xFF, 0xF1, 0xC5, 0x4A, 0xFF, 0xF1, 0xC4, 0x49, 0xFF, 0xF2, 0xC4, 0x47, 0xFF, 0xF2, 0xC3, 0x45, 0xFF, 0xF1, + 0xC3, 0x43, 0xFF, 0xF0, 0xC4, 0x40, 0xFF, 0xF3, 0xBF, 0x42, 0xFF, 0xF5, 0xC0, 0x39, 0xFF, 0xCA, 0xAC, 0x5E, 0xFF, + 0x1E, 0x58, 0xFA, 0xFF, 0x30, 0x6E, 0xF3, 0xFF, 0x35, 0x80, 0xF7, 0xFF, 0x3E, 0x92, 0xFB, 0xFF, 0x5D, 0xAF, 0xFB, + 0xFF, 0x72, 0xC2, 0xFF, 0xFF, 0xBA, 0xE1, 0xFD, 0xFF, 0x74, 0xCD, 0xFF, 0xFF, 0x71, 0xD3, 0xFF, 0xFF, 0x83, 0xE5, + 0xFF, 0xFF, 0x95, 0xF7, 0xFF, 0xFF, 0xA1, 0xF4, 0xFE, 0xFF, 0xAD, 0xF0, 0xFD, 0xFF, 0xC1, 0xF8, 0xFF, 0xFF, 0xCD, + 0xF7, 0xFB, 0xFF, 0xD1, 0xF8, 0xFE, 0xFF, 0xD6, 0xF9, 0xFF, 0xFF, 0xE0, 0xF6, 0xFE, 0xFF, 0xDD, 0xF5, 0xFB, 0xFF, + 0xED, 0xFB, 0xFF, 0xFF, 0xE8, 0xFB, 0xFB, 0xFF, 0xDF, 0xFC, 0xFF, 0xFF, 0xE8, 0xE0, 0xB2, 0xFF, 0xEF, 0xA3, 0x18, + 0xFF, 0xEC, 0xAA, 0x25, 0xFF, 0xF5, 0xA8, 0x15, 0xFF, 0xD8, 0xE3, 0xC2, 0xFF, 0xC5, 0xF9, 0xF9, 0xFF, 0xCA, 0xF5, + 0xEE, 0xFF, 0xCE, 0xF6, 0xEF, 0xFF, 0xD2, 0xF7, 0xF0, 0xFF, 0xD1, 0xF8, 0xF1, 0xFF, 0xD0, 0xF9, 0xF1, 0xFF, 0xCD, + 0xF9, 0xF1, 0xFF, 0xC9, 0xF9, 0xF1, 0xFF, 0xC9, 0xFB, 0xF2, 0xFF, 0xCA, 0xFC, 0xF4, 0xFF, 0xB6, 0xF8, 0xE6, 0xFF, + 0xA2, 0xF3, 0xD9, 0xFF, 0x89, 0xEF, 0xCA, 0xFF, 0x71, 0xEB, 0xBC, 0xFF, 0x61, 0xE6, 0xB0, 0xFF, 0x50, 0xE1, 0xA4, + 0xFF, 0x48, 0xD9, 0x99, 0xFF, 0x40, 0xD2, 0x8F, 0xFF, 0x3A, 0xC7, 0x83, 0xFF, 0x34, 0xBC, 0x77, 0xFF, 0x1C, 0xB2, + 0x6A, 0xFF, 0x04, 0xA9, 0x5D, 0xFF, 0xEA, 0x8D, 0x13, 0xFF, 0xEF, 0x93, 0x11, 0xFF, 0xEF, 0x92, 0x0F, 0xFF, 0xF0, + 0x92, 0x0E, 0xFF, 0xF0, 0x92, 0x0E, 0xFF, 0xF2, 0xC6, 0x4D, 0xFF, 0xF2, 0xC5, 0x4B, 0xFF, 0xF2, 0xC5, 0x4A, 0xFF, + 0xF2, 0xC5, 0x48, 0xFF, 0xF2, 0xC4, 0x46, 0xFF, 0xEE, 0xC4, 0x46, 0xFF, 0xEA, 0xC4, 0x46, 0xFF, 0xF2, 0xBF, 0x48, + 0xFF, 0xFB, 0xC6, 0x34, 0xFF, 0x91, 0x95, 0x98, 0xFF, 0x27, 0x64, 0xFC, 0xFF, 0x3B, 0x76, 0xF1, 0xFF, 0x32, 0x83, + 0xFC, 0xFF, 0x34, 0x91, 0xFF, 0xFF, 0x63, 0xB4, 0xFF, 0xFF, 0x5A, 0xBD, 0xFF, 0xFF, 0xB5, 0xDC, 0xF3, 0xFF, 0x97, + 0xD0, 0xCB, 0xFF, 0xA4, 0xCE, 0xB4, 0xFF, 0xB0, 0xD2, 0xAF, 0xFF, 0xBC, 0xD6, 0xAB, 0xFF, 0xBE, 0xE1, 0xC2, 0xFF, + 0xC0, 0xEB, 0xDA, 0xFF, 0xC7, 0xFC, 0xF5, 0xFF, 0xBD, 0xFE, 0xFF, 0xFF, 0xCC, 0xFD, 0xFF, 0xFF, 0xDB, 0xFC, 0xFF, + 0xFF, 0xE0, 0xFC, 0xFE, 0xFF, 0xE4, 0xFC, 0xFB, 0xFF, 0xE6, 0xFB, 0xFD, 0xFF, 0xE7, 0xFA, 0xFF, 0xFF, 0xDD, 0xFB, + 0xFF, 0xFF, 0xF4, 0xC4, 0x61, 0xFF, 0xEE, 0xAA, 0x26, 0xFF, 0xEB, 0xAA, 0x22, 0xFF, 0xF6, 0xA7, 0x10, 0xFF, 0xD6, + 0xFF, 0xFF, 0xFF, 0xCF, 0xF4, 0xF5, 0xFF, 0xD8, 0xF9, 0xF5, 0xFF, 0xD9, 0xF9, 0xF4, 0xFF, 0xD9, 0xF8, 0xF2, 0xFF, + 0xD8, 0xF9, 0xF3, 0xFF, 0xD6, 0xFB, 0xF4, 0xFF, 0xD5, 0xFC, 0xF5, 0xFF, 0xD3, 0xFD, 0xF6, 0xFF, 0xCD, 0xFA, 0xF3, + 0xFF, 0xC7, 0xF6, 0xEF, 0xFF, 0xB0, 0xF3, 0xE1, 0xFF, 0x98, 0xF0, 0xD3, 0xFF, 0x82, 0xED, 0xC5, 0xFF, 0x6B, 0xEB, + 0xB7, 0xFF, 0x5F, 0xE9, 0xB0, 0xFF, 0x53, 0xE7, 0xAA, 0xFF, 0x49, 0xDF, 0x9F, 0xFF, 0x3E, 0xD7, 0x93, 0xFF, 0x39, + 0xCB, 0x87, 0xFF, 0x34, 0xBF, 0x7B, 0xFF, 0x25, 0xB4, 0x6B, 0xFF, 0x32, 0xA2, 0x5B, 0xFF, 0xF9, 0x94, 0x04, 0xFF, + 0xED, 0x94, 0x17, 0xFF, 0xEC, 0x92, 0x11, 0xFF, 0xEB, 0x91, 0x0B, 0xFF, 0xEB, 0x91, 0x0B, 0xFF, 0xF2, 0xC7, 0x4E, + 0xFF, 0xF3, 0xC7, 0x4D, 0xFF, 0xF3, 0xC7, 0x4C, 0xFF, 0xF3, 0xC7, 0x4A, 0xFF, 0xF4, 0xC7, 0x49, 0xFF, 0xF1, 0xC4, + 0x47, 0xFF, 0xEE, 0xC1, 0x45, 0xFF, 0xF7, 0xC2, 0x42, 0xFF, 0xFF, 0xC8, 0x33, 0xFF, 0x46, 0x67, 0xDE, 0xFF, 0x2A, + 0x63, 0xFF, 0xFF, 0x1B, 0x6F, 0xFF, 0xFF, 0x52, 0x8B, 0xE0, 0xFF, 0x84, 0xA0, 0xA3, 0xFF, 0xCC, 0xC1, 0x62, 0xFF, + 0xFF, 0xC0, 0x26, 0xFF, 0xFF, 0xB7, 0x29, 0xFF, 0xF1, 0xB5, 0x24, 0xFF, 0xF9, 0xB7, 0x27, 0xFF, 0xF6, 0xB5, 0x25, + 0xFF, 0xF2, 0xB2, 0x23, 0xFF, 0xFA, 0xB5, 0x24, 0xFF, 0xFF, 0xB7, 0x24, 0xFF, 0xDE, 0x9D, 0x17, 0xFF, 0xF4, 0xBA, + 0x42, 0xFF, 0xE7, 0xDA, 0x9E, 0xFF, 0xDC, 0xF9, 0xF9, 0xFF, 0xE6, 0xFB, 0xF3, 0xFF, 0xE9, 0xFF, 0xFF, 0xFF, 0xE6, + 0xFF, 0xFD, 0xFF, 0xE2, 0xFB, 0xFA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xA7, 0x1D, 0xFF, 0xF0, 0xA7, 0x1C, 0xFF, + 0xF1, 0xA7, 0x1A, 0xFF, 0xF0, 0xC4, 0x5A, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xE1, 0xF9, 0xFA, 0xFF, 0xE2, 0xFB, 0xFA, + 0xFF, 0xDF, 0xFB, 0xF8, 0xFF, 0xDC, 0xFA, 0xF5, 0xFF, 0xDB, 0xFB, 0xF5, 0xFF, 0xD9, 0xFB, 0xF5, 0xFF, 0xD6, 0xFC, + 0xF5, 0xFF, 0xD3, 0xFD, 0xF5, 0xFF, 0xC8, 0xF8, 0xF0, 0xFF, 0xBD, 0xF4, 0xEA, 0xFF, 0xA8, 0xF1, 0xDF, 0xFF, 0x93, + 0xEF, 0xD4, 0xFF, 0x7A, 0xF3, 0xC6, 0xFF, 0x61, 0xF8, 0xB9, 0xFF, 0x57, 0xEF, 0xB0, 0xFF, 0x4D, 0xE6, 0xA6, 0xFF, + 0x48, 0xE2, 0xA3, 0xFF, 0x3A, 0xD6, 0x98, 0xFF, 0x37, 0xCD, 0x89, 0xFF, 0x35, 0xC3, 0x7B, 0xFF, 0x20, 0xB7, 0x6F, + 0xFF, 0x84, 0x9C, 0x3A, 0xFF, 0xF4, 0x93, 0x0C, 0xFF, 0xEC, 0x94, 0x13, 0xFF, 0xE9, 0x93, 0x11, 0xFF, 0xE6, 0x92, + 0x0F, 0xFF, 0xE6, 0x92, 0x0F, 0xFF, 0xF3, 0xC9, 0x50, 0xFF, 0xF4, 0xC9, 0x4F, 0xFF, 0xF4, 0xC9, 0x4E, 0xFF, 0xF5, + 0xCA, 0x4D, 0xFF, 0xF5, 0xCA, 0x4B, 0xFF, 0xF4, 0xC5, 0x48, 0xFF, 0xF3, 0xBF, 0x44, 0xFF, 0xEE, 0xC1, 0x47, 0xFF, + 0xEA, 0xC4, 0x4A, 0xFF, 0x1F, 0x52, 0xFF, 0xFF, 0x92, 0x9A, 0xA6, 0xFF, 0xE6, 0xB6, 0x51, 0xFF, 0xFF, 0xC7, 0x28, + 0xFF, 0xF8, 0xC4, 0x2C, 0xFF, 0xF0, 0xC0, 0x30, 0xFF, 0xEF, 0xBA, 0x3F, 0xFF, 0xEF, 0xBF, 0x37, 0xFF, 0xEF, 0xB9, + 0x38, 0xFF, 0xF0, 0xB2, 0x3A, 0xFF, 0xF3, 0xB5, 0x38, 0xFF, 0xF6, 0xB7, 0x35, 0xFF, 0xEF, 0xB9, 0x32, 0xFF, 0xE8, + 0xBB, 0x2F, 0xFF, 0xEA, 0xB8, 0x2F, 0xFF, 0xED, 0xB4, 0x2F, 0xFF, 0xF3, 0xAC, 0x1F, 0xFF, 0xF9, 0xA3, 0x10, 0xFF, + 0xF2, 0xC9, 0x6F, 0xFF, 0xDF, 0xF9, 0xF5, 0xFF, 0xDE, 0xFB, 0xF5, 0xFF, 0xDD, 0xFD, 0xF5, 0xFF, 0xE3, 0xEA, 0xD7, + 0xFF, 0xEE, 0xA5, 0x10, 0xFF, 0xF4, 0xB2, 0x2D, 0xFF, 0xF7, 0xA5, 0x13, 0xFF, 0xEB, 0xE1, 0xA5, 0xFF, 0xF8, 0xFF, + 0xFF, 0xFF, 0xF2, 0xFE, 0xFF, 0xFF, 0xEC, 0xFD, 0xFF, 0xFF, 0xE6, 0xFC, 0xFC, 0xFF, 0xDF, 0xFC, 0xF7, 0xFF, 0xDE, + 0xFC, 0xF7, 0xFF, 0xDC, 0xFC, 0xF6, 0xFF, 0xD7, 0xFC, 0xF5, 0xFF, 0xD3, 0xFC, 0xF4, 0xFF, 0xC3, 0xF7, 0xED, 0xFF, + 0xB4, 0xF1, 0xE5, 0xFF, 0xB7, 0xF5, 0xE4, 0xFF, 0xBB, 0xF9, 0xE4, 0xFF, 0xD2, 0xFE, 0xEB, 0xFF, 0xE9, 0xFF, 0xF2, + 0xFF, 0xDB, 0xFE, 0xED, 0xFF, 0xCD, 0xF9, 0xE8, 0xFF, 0x89, 0xEF, 0xCA, 0xFF, 0x35, 0xD6, 0x9C, 0xFF, 0x2D, 0xC6, + 0x83, 0xFF, 0x25, 0xB7, 0x6B, 0xFF, 0x14, 0xB3, 0x6C, 0xFF, 0xD6, 0x95, 0x1A, 0xFF, 0xEE, 0x91, 0x15, 0xFF, 0xEB, + 0x93, 0x0F, 0xFF, 0xE6, 0x93, 0x10, 0xFF, 0xE0, 0x93, 0x12, 0xFF, 0xE0, 0x93, 0x12, 0xFF, 0xF4, 0xCA, 0x52, 0xFF, + 0xF4, 0xCA, 0x50, 0xFF, 0xF3, 0xCA, 0x4E, 0xFF, 0xF3, 0xC9, 0x4C, 0xFF, 0xF3, 0xC9, 0x4A, 0xFF, 0xF4, 0xC8, 0x48, + 0xFF, 0xF6, 0xC6, 0x46, 0xFF, 0xEC, 0xBF, 0x3F, 0xFF, 0xEB, 0xBF, 0x41, 0xFF, 0xF8, 0xD4, 0x40, 0xFF, 0xFC, 0xC9, + 0x33, 0xFF, 0xFF, 0xC9, 0x2F, 0xFF, 0xEC, 0xC2, 0x42, 0xFF, 0xF4, 0xC3, 0x40, 0xFF, 0xFC, 0xC3, 0x3E, 0xFF, 0xF3, + 0xBB, 0x34, 0xFF, 0xF2, 0xBB, 0x33, 0xFF, 0xF6, 0xBD, 0x49, 0xFF, 0xF8, 0xB7, 0x38, 0xFF, 0xF5, 0xB7, 0x36, 0xFF, + 0xF2, 0xB7, 0x34, 0xFF, 0xF3, 0xB5, 0x2E, 0xFF, 0xF5, 0xB3, 0x27, 0xFF, 0xF7, 0xBA, 0x2F, 0xFF, 0xF2, 0xBA, 0x2F, + 0xFF, 0xF1, 0xB5, 0x30, 0xFF, 0xF0, 0xB0, 0x31, 0xFF, 0xF6, 0xAC, 0x1E, 0xFF, 0xED, 0xAA, 0x0C, 0xFF, 0xEC, 0xD2, + 0x7E, 0xFF, 0xE6, 0xFF, 0xFF, 0xFF, 0xD2, 0xD9, 0x80, 0xFF, 0xF8, 0xA9, 0x2E, 0xFF, 0xEB, 0xAF, 0x1C, 0xFF, 0xE5, + 0xAA, 0x02, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xF9, 0xFE, 0xFF, 0xFF, 0xF4, 0xFD, 0xFF, 0xFF, + 0xEB, 0xFD, 0xFD, 0xFF, 0xE2, 0xFE, 0xFA, 0xFF, 0xE1, 0xFD, 0xF9, 0xFF, 0xE0, 0xFC, 0xF7, 0xFF, 0xD7, 0xFC, 0xF5, + 0xFF, 0xCF, 0xFD, 0xF3, 0xFF, 0xE2, 0xFB, 0xF4, 0xFF, 0xE7, 0xFD, 0xF6, 0xFF, 0xE8, 0xFD, 0xF3, 0xFF, 0xE9, 0xFD, + 0xF0, 0xFF, 0xD3, 0xFD, 0xEB, 0xFF, 0xBD, 0xFC, 0xE5, 0xFF, 0xBA, 0xF7, 0xDF, 0xFF, 0xB6, 0xF2, 0xDA, 0xFF, 0xD2, + 0xFB, 0xE9, 0xFF, 0xE6, 0xFC, 0xF1, 0xFF, 0x8D, 0xDE, 0xB6, 0xFF, 0x3C, 0xC7, 0x84, 0xFF, 0x47, 0xB7, 0x99, 0xFF, + 0xF8, 0xA1, 0x13, 0xFF, 0xF2, 0x94, 0x04, 0xFF, 0xEE, 0x94, 0x10, 0xFF, 0xEC, 0x94, 0x10, 0xFF, 0xE9, 0x95, 0x10, + 0xFF, 0xE9, 0x95, 0x10, 0xFF, 0xF5, 0xCC, 0x53, 0xFF, 0xF3, 0xCB, 0x50, 0xFF, 0xF2, 0xCA, 0x4E, 0xFF, 0xF1, 0xC9, + 0x4B, 0xFF, 0xF0, 0xC7, 0x48, 0xFF, 0xF4, 0xCB, 0x48, 0xFF, 0xF9, 0xCE, 0x47, 0xFF, 0xF2, 0xC4, 0x40, 0xFF, 0xFC, + 0xCA, 0x48, 0xFF, 0xF0, 0xC2, 0x3F, 0xFF, 0xF5, 0xC9, 0x46, 0xFF, 0xF4, 0xC7, 0x46, 0xFF, 0xF3, 0xC4, 0x45, 0xFF, + 0xED, 0xB4, 0x38, 0xFF, 0xE8, 0xA5, 0x2C, 0xFF, 0xE1, 0xB0, 0x2E, 0xFF, 0xEA, 0xC0, 0x56, 0xFF, 0xE9, 0xC8, 0x6C, + 0xFF, 0xE4, 0xC1, 0x36, 0xFF, 0xEB, 0xC9, 0x50, 0xFF, 0xF1, 0xD1, 0x6A, 0xFF, 0xF5, 0xD0, 0x73, 0xFF, 0xF9, 0xCF, + 0x7D, 0xFF, 0xF8, 0xC7, 0x56, 0xFF, 0xE7, 0xAF, 0x1F, 0xFF, 0xED, 0xB1, 0x25, 0xFF, 0xF4, 0xB2, 0x2B, 0xFF, 0xF9, + 0xB5, 0x3E, 0xFF, 0xEE, 0xB3, 0x2A, 0xFF, 0xF5, 0xAF, 0x1B, 0xFF, 0xF0, 0xB5, 0x32, 0xFF, 0xF9, 0xB1, 0x3F, 0xFF, + 0xF2, 0xA9, 0x26, 0xFF, 0xEA, 0xAE, 0x1F, 0xFF, 0xF3, 0xB8, 0x3F, 0xFF, 0xF3, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFB, 0xFD, 0xFE, 0xFF, 0xF0, 0xFE, 0xFE, 0xFF, 0xE5, 0xFF, 0xFD, 0xFF, 0xE4, 0xFE, + 0xFB, 0xFF, 0xE3, 0xFC, 0xF8, 0xFF, 0xD7, 0xFD, 0xF5, 0xFF, 0xCB, 0xFD, 0xF2, 0xFF, 0xEB, 0xFB, 0xF4, 0xFF, 0xEE, + 0xFE, 0xF6, 0xFF, 0xDE, 0xFD, 0xF1, 0xFF, 0xCE, 0xFB, 0xED, 0xFF, 0xB0, 0xF9, 0xE2, 0xFF, 0x91, 0xF6, 0xD8, 0xFF, + 0x8A, 0xF3, 0xD2, 0xFF, 0x83, 0xF1, 0xCC, 0xFF, 0x96, 0xEE, 0xCE, 0xFF, 0xA9, 0xEA, 0xD0, 0xFF, 0xC0, 0xEA, 0xDA, + 0xFF, 0xE8, 0xFA, 0xF4, 0xFF, 0x78, 0xC6, 0x7E, 0xFF, 0xFF, 0xC0, 0x59, 0xFF, 0xEA, 0xA0, 0x19, 0xFF, 0xF2, 0x95, + 0x10, 0xFF, 0xF2, 0x96, 0x0F, 0xFF, 0xF2, 0x96, 0x0D, 0xFF, 0xF2, 0x96, 0x0D, 0xFF, 0xF4, 0xCD, 0x54, 0xFF, 0xF4, + 0xCB, 0x51, 0xFF, 0xF3, 0xCA, 0x4F, 0xFF, 0xF2, 0xC9, 0x4C, 0xFF, 0xF2, 0xC8, 0x4A, 0xFF, 0xF1, 0xC6, 0x48, 0xFF, + 0xF1, 0xC4, 0x47, 0xFF, 0xF3, 0xD2, 0x48, 0xFF, 0xF3, 0xC7, 0x46, 0xFF, 0xFB, 0xC5, 0x4C, 0xFF, 0xDC, 0x9A, 0x2B, + 0xFF, 0xCD, 0x83, 0x17, 0xFF, 0xBE, 0x6B, 0x03, 0xFF, 0xC5, 0x7F, 0x00, 0xFF, 0xD4, 0x96, 0x0E, 0xFF, 0xDB, 0xAC, + 0x2E, 0xFF, 0xEA, 0xC5, 0x60, 0xFF, 0xEF, 0xCC, 0x75, 0xFF, 0xEA, 0xCA, 0x51, 0xFF, 0xEF, 0xD2, 0x69, 0xFF, 0xF5, + 0xDA, 0x81, 0xFF, 0xF7, 0xE4, 0x99, 0xFF, 0xF9, 0xEE, 0xB2, 0xFF, 0xFF, 0xFA, 0xCE, 0xFF, 0xFF, 0xFE, 0xE2, 0xFF, + 0xFF, 0xE1, 0x99, 0xFF, 0xF7, 0xBC, 0x48, 0xFF, 0xDC, 0xB4, 0x10, 0xFF, 0xF0, 0xAD, 0x31, 0xFF, 0xFB, 0xAC, 0x27, + 0xFF, 0xF3, 0xB2, 0x30, 0xFF, 0xF5, 0xB1, 0x34, 0xFF, 0xF0, 0xAD, 0x24, 0xFF, 0xF6, 0xAC, 0x26, 0xFF, 0xFC, 0xD1, + 0x97, 0xFF, 0xF7, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFB, 0xFF, 0xFE, 0xFF, 0xF3, 0xFF, 0xFE, 0xFF, 0xED, + 0xFF, 0xFD, 0xFF, 0xE7, 0xFD, 0xFC, 0xFF, 0xE3, 0xFE, 0xFB, 0xFF, 0xDF, 0xFE, 0xF9, 0xFF, 0xE7, 0xFD, 0xF8, 0xFF, + 0xEF, 0xFC, 0xF7, 0xFF, 0xEB, 0xFB, 0xF3, 0xFF, 0xD8, 0xFD, 0xEF, 0xFF, 0xC2, 0xFA, 0xE8, 0xFF, 0xAB, 0xF8, 0xE2, + 0xFF, 0x9B, 0xF4, 0xD8, 0xFF, 0x8A, 0xEF, 0xCE, 0xFF, 0x76, 0xEA, 0xC1, 0xFF, 0x61, 0xE5, 0xB4, 0xFF, 0x5A, 0xDD, + 0xAB, 0xFF, 0x61, 0xD2, 0xA2, 0xFF, 0x8D, 0xE9, 0xC1, 0xFF, 0xB8, 0xE7, 0xDA, 0xFF, 0xFF, 0xD4, 0x96, 0xFF, 0xFA, + 0xD0, 0x8E, 0xFF, 0xED, 0xAD, 0x41, 0xFF, 0xF1, 0x95, 0x10, 0xFF, 0xF1, 0x95, 0x0F, 0xFF, 0xF1, 0x96, 0x0E, 0xFF, + 0xF1, 0x96, 0x0E, 0xFF, 0xF4, 0xCD, 0x54, 0xFF, 0xF4, 0xCC, 0x52, 0xFF, 0xF4, 0xCB, 0x50, 0xFF, 0xF3, 0xC9, 0x4E, + 0xFF, 0xF3, 0xC8, 0x4B, 0xFF, 0xF6, 0xC9, 0x51, 0xFF, 0xFA, 0xCA, 0x56, 0xFF, 0xEA, 0xC0, 0x44, 0xFF, 0xC6, 0x74, + 0x19, 0xFF, 0xAD, 0x58, 0x00, 0xFF, 0xB3, 0x5B, 0x01, 0xFF, 0xC0, 0x6F, 0x06, 0xFF, 0xCC, 0x84, 0x0B, 0xFF, 0xCE, + 0x93, 0x00, 0xFF, 0xDF, 0xA7, 0x11, 0xFF, 0xE5, 0xB9, 0x3E, 0xFF, 0xEB, 0xCA, 0x6A, 0xFF, 0xF5, 0xD1, 0x7E, 0xFF, + 0xF0, 0xD3, 0x6B, 0xFF, 0xF4, 0xDB, 0x81, 0xFF, 0xF8, 0xE3, 0x97, 0xFF, 0xF7, 0xEB, 0xA4, 0xFF, 0xF5, 0xF4, 0xB1, + 0xFF, 0xF9, 0xF7, 0xC7, 0xFF, 0xFC, 0xFA, 0xDC, 0xFF, 0xFF, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xF8, 0xFF, 0xFD, 0xEB, + 0xBB, 0xFF, 0xF2, 0xB4, 0x22, 0xFF, 0xFF, 0xAF, 0x28, 0xFF, 0xF6, 0xB0, 0x2F, 0xFF, 0xF2, 0xB0, 0x29, 0xFF, 0xEE, + 0xB1, 0x22, 0xFF, 0xF9, 0xA7, 0x19, 0xFF, 0xF4, 0xE6, 0xC9, 0xFF, 0xF4, 0xF7, 0xF7, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, + 0xF6, 0xFF, 0xFE, 0xFF, 0xEC, 0xFF, 0xFD, 0xFF, 0xEA, 0xFF, 0xFC, 0xFF, 0xE8, 0xFA, 0xFA, 0xFF, 0xE2, 0xFD, 0xFB, + 0xFF, 0xDC, 0xFF, 0xFB, 0xFF, 0xE9, 0xFF, 0xFB, 0xFF, 0xF6, 0xFF, 0xFB, 0xFF, 0xDC, 0xFD, 0xF1, 0xFF, 0xC3, 0xFB, + 0xE7, 0xFF, 0xB4, 0xF5, 0xDF, 0xFF, 0xA5, 0xF0, 0xD8, 0xFF, 0x94, 0xEC, 0xCE, 0xFF, 0x83, 0xE8, 0xC4, 0xFF, 0x77, + 0xE5, 0xB7, 0xFF, 0x6B, 0xE3, 0xAB, 0xFF, 0x52, 0xDE, 0xA0, 0xFF, 0x55, 0xD4, 0x94, 0xFF, 0x40, 0xBD, 0x7F, 0xFF, + 0x98, 0xE4, 0xD1, 0xFF, 0xF4, 0xA1, 0x2B, 0xFF, 0xF6, 0xA1, 0x2F, 0xFF, 0xF3, 0x9B, 0x1F, 0xFF, 0xF0, 0x95, 0x0F, + 0xFF, 0xF0, 0x95, 0x0F, 0xFF, 0xF0, 0x95, 0x0F, 0xFF, 0xF0, 0x95, 0x0F, 0xFF, 0xF4, 0xCE, 0x55, 0xFF, 0xF4, 0xCC, + 0x53, 0xFF, 0xF4, 0xCB, 0x51, 0xFF, 0xF5, 0xCA, 0x4F, 0xFF, 0xF6, 0xC9, 0x4E, 0xFF, 0xF4, 0xC9, 0x4D, 0xFF, 0xFA, + 0xD0, 0x53, 0xFF, 0xCD, 0x86, 0x2A, 0xFF, 0xB0, 0x52, 0x06, 0xFF, 0xB8, 0x5F, 0x04, 0xFF, 0xC8, 0x73, 0x0A, 0xFF, + 0xCE, 0x82, 0x08, 0xFF, 0xD3, 0x91, 0x06, 0xFF, 0xD5, 0xA0, 0x01, 0xFF, 0xE6, 0xB4, 0x24, 0xFF, 0xEA, 0xC4, 0x4C, + 0xFF, 0xED, 0xD3, 0x74, 0xFF, 0xF4, 0xD9, 0x83, 0xFF, 0xF3, 0xDC, 0x7E, 0xFF, 0xF6, 0xE4, 0x93, 0xFF, 0xF8, 0xEC, + 0xA8, 0xFF, 0xF9, 0xF2, 0xB5, 0xFF, 0xF9, 0xF8, 0xC3, 0xFF, 0xFA, 0xFA, 0xD3, 0xFF, 0xFB, 0xFB, 0xE2, 0xFF, 0xFB, + 0xFE, 0xED, 0xFF, 0xF3, 0xF9, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xEE, 0xDC, 0x7E, 0xFF, + 0xFD, 0xAD, 0x26, 0xFF, 0xF7, 0xAF, 0x29, 0xFF, 0xF1, 0xB1, 0x2D, 0xFF, 0xDF, 0xB1, 0x34, 0xFF, 0xF6, 0xA6, 0x09, + 0xFF, 0xF4, 0xD3, 0x8C, 0xFF, 0xF8, 0xFB, 0xFC, 0xFF, 0xF6, 0xFF, 0xFF, 0xFF, 0xEB, 0xFF, 0xFD, 0xFF, 0xE5, 0xFE, + 0xFC, 0xFF, 0xE0, 0xFB, 0xFB, 0xFF, 0xDE, 0xFC, 0xF9, 0xFF, 0xDC, 0xFC, 0xF7, 0xFF, 0xEF, 0xFF, 0xFC, 0xFF, 0xEB, + 0xFC, 0xF8, 0xFF, 0xD0, 0xF5, 0xE8, 0xFF, 0xBC, 0xF5, 0xDF, 0xFF, 0xAC, 0xF1, 0xD8, 0xFF, 0x9D, 0xED, 0xD2, 0xFF, + 0x7D, 0xE8, 0xC4, 0xFF, 0x6C, 0xE1, 0xB7, 0xFF, 0x5E, 0xDC, 0xAB, 0xFF, 0x4F, 0xD7, 0x9E, 0xFF, 0x5E, 0xC9, 0x98, + 0xFF, 0x35, 0xC6, 0x92, 0xFF, 0x42, 0xC9, 0x8B, 0xFF, 0x4D, 0xB2, 0x80, 0xFF, 0xF1, 0x9B, 0x00, 0xFF, 0xF8, 0x93, + 0x17, 0xFF, 0xF4, 0x95, 0x15, 0xFF, 0xF1, 0x97, 0x12, 0xFF, 0xF0, 0x96, 0x11, 0xFF, 0xEF, 0x95, 0x10, 0xFF, 0xEF, + 0x95, 0x10, 0xFF, 0xF4, 0xCE, 0x55, 0xFF, 0xF4, 0xCD, 0x54, 0xFF, 0xF5, 0xCC, 0x52, 0xFF, 0xF6, 0xCB, 0x51, 0xFF, + 0xF8, 0xCB, 0x50, 0xFF, 0xF1, 0xC8, 0x49, 0xFF, 0xF9, 0xD5, 0x51, 0xFF, 0xC0, 0x62, 0x15, 0xFF, 0xBB, 0x5C, 0x00, + 0xFF, 0xCC, 0x74, 0x07, 0xFF, 0xCD, 0x7C, 0x02, 0xFF, 0xD4, 0x8D, 0x02, 0xFF, 0xDB, 0x9E, 0x01, 0xFF, 0xDC, 0xAD, + 0x08, 0xFF, 0xED, 0xC1, 0x36, 0xFF, 0xEE, 0xCF, 0x5A, 0xFF, 0xF0, 0xDC, 0x7D, 0xFF, 0xF3, 0xE1, 0x87, 0xFF, 0xF7, + 0xE6, 0x91, 0xFF, 0xF8, 0xED, 0xA5, 0xFF, 0xF8, 0xF5, 0xB8, 0xFF, 0xFB, 0xF9, 0xC6, 0xFF, 0xFD, 0xFD, 0xD4, 0xFF, + 0xFB, 0xFC, 0xDF, 0xFF, 0xFA, 0xFC, 0xE9, 0xFF, 0xFD, 0xFE, 0xF0, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFE, 0xFF, 0xFA, + 0xFF, 0xFB, 0xFE, 0xFC, 0xFF, 0xFF, 0xFA, 0xFD, 0xFF, 0xE7, 0xAF, 0x1D, 0xFF, 0xEE, 0xB0, 0x2A, 0xFF, 0xF5, 0xB1, + 0x37, 0xFF, 0xF6, 0xB8, 0x24, 0xFF, 0xF7, 0xB4, 0x28, 0xFF, 0xF4, 0xAF, 0x21, 0xFF, 0xF2, 0xAA, 0x1A, 0xFF, 0xF5, + 0xD7, 0x9E, 0xFF, 0xE9, 0xFF, 0xFC, 0xFF, 0xE0, 0xFE, 0xFC, 0xFF, 0xD7, 0xFD, 0xFC, 0xFF, 0xDA, 0xFA, 0xF8, 0xFF, + 0xDD, 0xF7, 0xF3, 0xFF, 0xF4, 0xFD, 0xFD, 0xFF, 0xE0, 0xF9, 0xF6, 0xFF, 0xC3, 0xEC, 0xDF, 0xFF, 0xB5, 0xEF, 0xD7, + 0xFF, 0xA5, 0xEC, 0xD2, 0xFF, 0x95, 0xE9, 0xCC, 0xFF, 0x67, 0xE5, 0xBB, 0xFF, 0x55, 0xDB, 0xAB, 0xFF, 0x44, 0xD3, + 0x9E, 0xFF, 0x32, 0xCB, 0x91, 0xFF, 0x24, 0xC8, 0x85, 0xFF, 0x6A, 0xB4, 0x79, 0xFF, 0xAF, 0x9D, 0x3A, 0xFF, 0xFF, + 0x97, 0x0B, 0xFF, 0xF9, 0x93, 0x18, 0xFF, 0xED, 0x9B, 0x0F, 0xFF, 0xF0, 0x9A, 0x12, 0xFF, 0xF3, 0x98, 0x15, 0xFF, + 0xF1, 0x96, 0x13, 0xFF, 0xEF, 0x94, 0x11, 0xFF, 0xEF, 0x94, 0x11, 0xFF, 0xF4, 0xCF, 0x58, 0xFF, 0xF4, 0xCE, 0x55, + 0xFF, 0xF4, 0xCD, 0x53, 0xFF, 0xF6, 0xCC, 0x52, 0xFF, 0xF8, 0xCB, 0x52, 0xFF, 0xFA, 0xD5, 0x52, 0xFF, 0xFB, 0xC7, + 0x4E, 0xFF, 0xAD, 0x4C, 0x00, 0xFF, 0xCA, 0x6F, 0x09, 0xFF, 0xD3, 0x7F, 0x0B, 0xFF, 0xD4, 0x88, 0x05, 0xFF, 0xDB, + 0x97, 0x04, 0xFF, 0xE1, 0xA7, 0x04, 0xFF, 0xE5, 0xB6, 0x18, 0xFF, 0xF1, 0xC7, 0x3F, 0xFF, 0xF3, 0xD3, 0x62, 0xFF, + 0xF4, 0xDF, 0x86, 0xFF, 0xF7, 0xE4, 0x91, 0xFF, 0xF9, 0xE9, 0x9B, 0xFF, 0xF9, 0xF0, 0xAD, 0xFF, 0xF9, 0xF7, 0xBF, + 0xFF, 0xFB, 0xFA, 0xCB, 0xFF, 0xFD, 0xFC, 0xD7, 0xFF, 0xFC, 0xFD, 0xDE, 0xFF, 0xFB, 0xFD, 0xE5, 0xFF, 0xFE, 0xFF, + 0xEF, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFA, 0xFE, 0xF2, 0xFF, 0xFC, 0xFE, 0xFE, 0xFF, 0xFB, 0xE9, 0xC6, 0xFF, 0xEC, + 0xAF, 0x1D, 0xFF, 0xF6, 0xB4, 0x30, 0xFF, 0xF8, 0xB6, 0x2F, 0xFF, 0xF6, 0xA7, 0x19, 0xFF, 0xF0, 0xB0, 0x26, 0xFF, + 0xF2, 0xAD, 0x22, 0xFF, 0xF5, 0xAB, 0x1D, 0xFF, 0xF9, 0xA9, 0x26, 0xFF, 0xF6, 0xA6, 0x1C, 0xFF, 0xE9, 0xCD, 0x7D, + 0xFF, 0xDC, 0xF4, 0xDF, 0xFF, 0xAF, 0xFE, 0xEA, 0xFF, 0xED, 0xFD, 0xFD, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xD3, 0xF8, + 0xFB, 0xFF, 0xB4, 0xEE, 0xEC, 0xFF, 0xAB, 0xE9, 0xE6, 0xFF, 0x89, 0xE6, 0xD8, 0xFF, 0x67, 0xE2, 0xCB, 0xFF, 0x52, + 0xE1, 0xB8, 0xFF, 0x4C, 0xDD, 0xA6, 0xFF, 0x7E, 0xC5, 0x74, 0xFF, 0xB0, 0xAD, 0x42, 0xFF, 0xF3, 0x9B, 0x22, 0xFF, + 0xFF, 0x9C, 0x09, 0xFF, 0xF5, 0x98, 0x09, 0xFF, 0xEE, 0x9C, 0x10, 0xFF, 0xED, 0x99, 0x17, 0xFF, 0xED, 0x9D, 0x14, + 0xFF, 0xEF, 0x9B, 0x14, 0xFF, 0xF2, 0x99, 0x15, 0xFF, 0xF0, 0x97, 0x13, 0xFF, 0xEE, 0x95, 0x11, 0xFF, 0xEE, 0x95, + 0x11, 0xFF, 0xF5, 0xD0, 0x5A, 0xFF, 0xF4, 0xCF, 0x57, 0xFF, 0xF3, 0xCE, 0x54, 0xFF, 0xF5, 0xCC, 0x53, 0xFF, 0xF7, + 0xCB, 0x53, 0xFF, 0xF4, 0xD3, 0x4C, 0xFF, 0xDD, 0x9A, 0x2C, 0xFF, 0xC1, 0x5D, 0x03, 0xFF, 0xC8, 0x72, 0x05, 0xFF, + 0xD2, 0x83, 0x06, 0xFF, 0xDC, 0x93, 0x07, 0xFF, 0xE1, 0xA2, 0x07, 0xFF, 0xE7, 0xB0, 0x08, 0xFF, 0xEE, 0xBF, 0x27, + 0xFF, 0xF6, 0xCD, 0x47, 0xFF, 0xF7, 0xD8, 0x6B, 0xFF, 0xF9, 0xE2, 0x8E, 0xFF, 0xFA, 0xE7, 0x9A, 0xFF, 0xFB, 0xEC, + 0xA6, 0xFF, 0xFA, 0xF3, 0xB6, 0xFF, 0xFA, 0xF9, 0xC7, 0xFF, 0xFB, 0xFB, 0xD0, 0xFF, 0xFD, 0xFC, 0xD9, 0xFF, 0xFC, + 0xFD, 0xDD, 0xFF, 0xFC, 0xFE, 0xE2, 0xFF, 0xFE, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xF7, 0xFD, 0xEA, 0xFF, + 0xFE, 0xFE, 0xFF, 0xFF, 0xF7, 0xD7, 0x8F, 0xFF, 0xF1, 0xAF, 0x1E, 0xFF, 0xF6, 0xB0, 0x2E, 0xFF, 0xEB, 0xAB, 0x17, + 0xFF, 0xFD, 0xF7, 0xDF, 0xFF, 0xE9, 0xAC, 0x24, 0xFF, 0xF0, 0xAC, 0x22, 0xFF, 0xF8, 0xAC, 0x21, 0xFF, 0xF6, 0xAE, + 0x26, 0xFF, 0xF5, 0xB0, 0x2B, 0xFF, 0xF4, 0xA9, 0x19, 0xFF, 0xF3, 0xA2, 0x08, 0xFF, 0xF9, 0xA7, 0x22, 0xFF, 0xF2, + 0xC1, 0x4C, 0xFF, 0xEE, 0xCD, 0x6D, 0xFF, 0xDB, 0xC9, 0x7D, 0xFF, 0xC2, 0xCA, 0x7F, 0xFF, 0xC6, 0xC5, 0x81, 0xFF, + 0xCB, 0xBC, 0x60, 0xFF, 0xCF, 0xB3, 0x40, 0xFF, 0xE9, 0xA7, 0x24, 0xFF, 0xFF, 0x9B, 0x07, 0xFF, 0xFF, 0x9D, 0x10, + 0xFF, 0xFF, 0x9F, 0x1A, 0xFF, 0xE9, 0x98, 0x0F, 0xFF, 0xF9, 0x9C, 0x14, 0xFF, 0xF7, 0x9C, 0x14, 0xFF, 0xF4, 0x9B, + 0x14, 0xFF, 0xF1, 0x9D, 0x17, 0xFF, 0xED, 0x9E, 0x19, 0xFF, 0xEF, 0x9C, 0x16, 0xFF, 0xF1, 0x99, 0x14, 0xFF, 0xEF, + 0x97, 0x12, 0xFF, 0xED, 0x95, 0x10, 0xFF, 0xED, 0x95, 0x10, 0xFF, 0xF6, 0xD1, 0x5C, 0xFF, 0xF4, 0xD0, 0x58, 0xFF, + 0xF3, 0xCF, 0x55, 0xFF, 0xF5, 0xCD, 0x54, 0xFF, 0xF7, 0xCC, 0x53, 0xFF, 0xF6, 0xD5, 0x51, 0xFF, 0xCE, 0x7B, 0x16, + 0xFF, 0xC6, 0x67, 0x03, 0xFF, 0xCF, 0x7B, 0x06, 0xFF, 0xD7, 0x8B, 0x05, 0xFF, 0xDF, 0x9B, 0x05, 0xFF, 0xE4, 0xA8, + 0x07, 0xFF, 0xEA, 0xB6, 0x09, 0xFF, 0xF1, 0xC3, 0x2A, 0xFF, 0xF7, 0xD1, 0x4C, 0xFF, 0xF8, 0xDB, 0x6C, 0xFF, 0xFA, + 0xE4, 0x8D, 0xFF, 0xFA, 0xEA, 0x9C, 0xFF, 0xFB, 0xEF, 0xAB, 0xFF, 0xFA, 0xF5, 0xBC, 0xFF, 0xFA, 0xFA, 0xCD, 0xFF, + 0xFB, 0xFB, 0xD4, 0xFF, 0xFD, 0xFC, 0xDB, 0xFF, 0xFC, 0xFD, 0xDC, 0xFF, 0xFC, 0xFE, 0xDD, 0xFF, 0xFC, 0xFE, 0xE3, + 0xFF, 0xFD, 0xFE, 0xEA, 0xFF, 0xFD, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDE, 0xC0, 0x27, 0xFF, 0xF5, 0xB4, + 0x26, 0xFF, 0xF8, 0xB0, 0x1E, 0xFF, 0xFF, 0xC6, 0x4D, 0xFF, 0xEF, 0xF8, 0xFF, 0xFF, 0xFA, 0xFF, 0xFE, 0xFF, 0xF6, + 0xD8, 0x8B, 0xFF, 0xF3, 0xA7, 0x18, 0xFF, 0xF4, 0xA9, 0x1D, 0xFF, 0xF5, 0xAC, 0x22, 0xFF, 0xF2, 0xAB, 0x22, 0xFF, + 0xF0, 0xAB, 0x22, 0xFF, 0xF2, 0xA3, 0x1A, 0xFF, 0xEE, 0xA6, 0x1A, 0xFF, 0xF4, 0xA8, 0x17, 0xFF, 0xF3, 0xA2, 0x0D, + 0xFF, 0xF2, 0xA4, 0x10, 0xFF, 0xFF, 0xA3, 0x14, 0xFF, 0xFC, 0xA3, 0x15, 0xFF, 0xF9, 0xA2, 0x16, 0xFF, 0xF2, 0xA2, + 0x17, 0xFF, 0xEC, 0xA1, 0x18, 0xFF, 0xFD, 0x99, 0x0D, 0xFF, 0xED, 0x9A, 0x16, 0xFF, 0xFF, 0xA0, 0x00, 0xFF, 0xE8, + 0x9C, 0x2B, 0xFF, 0xAF, 0xB5, 0x60, 0xFF, 0xF7, 0x99, 0x10, 0xFF, 0xF2, 0x9B, 0x14, 0xFF, 0xED, 0x9D, 0x18, 0xFF, + 0xEE, 0x9B, 0x16, 0xFF, 0xEF, 0x99, 0x13, 0xFF, 0xED, 0x97, 0x11, 0xFF, 0xEB, 0x95, 0x0F, 0xFF, 0xEB, 0x95, 0x0F, + 0xFF, 0xF7, 0xD2, 0x5E, 0xFF, 0xF4, 0xD1, 0x5A, 0xFF, 0xF2, 0xD0, 0x56, 0xFF, 0xF5, 0xCE, 0x54, 0xFF, 0xF7, 0xCC, + 0x53, 0xFF, 0xF7, 0xD7, 0x56, 0xFF, 0xC0, 0x5B, 0x00, 0xFF, 0xCB, 0x70, 0x03, 0xFF, 0xD6, 0x84, 0x06, 0xFF, 0xDC, + 0x94, 0x05, 0xFF, 0xE2, 0xA3, 0x03, 0xFF, 0xE8, 0xAF, 0x07, 0xFF, 0xEE, 0xBB, 0x0B, 0xFF, 0xF3, 0xC8, 0x2D, 0xFF, + 0xF8, 0xD5, 0x50, 0xFF, 0xF9, 0xDE, 0x6E, 0xFF, 0xFA, 0xE6, 0x8C, 0xFF, 0xFB, 0xEC, 0x9F, 0xFF, 0xFB, 0xF2, 0xB1, + 0xFF, 0xFA, 0xF7, 0xC2, 0xFF, 0xF9, 0xFB, 0xD3, 0xFF, 0xFB, 0xFC, 0xD8, 0xFF, 0xFD, 0xFC, 0xDD, 0xFF, 0xFC, 0xFD, + 0xDB, 0xFF, 0xFC, 0xFE, 0xD8, 0xFF, 0xFB, 0xFD, 0xD8, 0xFF, 0xFA, 0xFC, 0xD9, 0xFF, 0xFA, 0xFA, 0xE4, 0xFF, 0xF6, + 0xE9, 0xA3, 0xFF, 0xFB, 0xAC, 0x2A, 0xFF, 0xFA, 0xB9, 0x2E, 0xFF, 0xED, 0xAD, 0x1A, 0xFF, 0xF7, 0xDA, 0x99, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFD, 0xFE, 0xFF, 0xFC, 0xFE, 0xFE, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xF9, 0xD4, 0x8C, + 0xFF, 0xF5, 0xA8, 0x19, 0xFF, 0xF7, 0xA9, 0x17, 0xFF, 0xF8, 0xA9, 0x16, 0xFF, 0xF3, 0xA7, 0x1A, 0xFF, 0xED, 0xA5, + 0x1E, 0xFF, 0xF1, 0xA7, 0x1F, 0xFF, 0xF6, 0xA9, 0x20, 0xFF, 0xF6, 0xA7, 0x1D, 0xFF, 0xF6, 0xA5, 0x1A, 0xFF, 0xF8, + 0xA3, 0x16, 0xFF, 0xFA, 0xA1, 0x12, 0xFF, 0xFC, 0x9D, 0x0A, 0xFF, 0xFE, 0x98, 0x03, 0xFF, 0xF9, 0xA1, 0x25, 0xFF, + 0xB0, 0xC0, 0x6F, 0xFF, 0x5D, 0xC9, 0xCF, 0xFF, 0x27, 0xE5, 0xFF, 0xFF, 0xB3, 0xB4, 0x73, 0xFF, 0xF9, 0x97, 0x0B, + 0xFF, 0xF3, 0x9A, 0x11, 0xFF, 0xED, 0x9D, 0x17, 0xFF, 0xEE, 0x9B, 0x15, 0xFF, 0xEE, 0x9A, 0x13, 0xFF, 0xEC, 0x98, + 0x11, 0xFF, 0xEA, 0x96, 0x0F, 0xFF, 0xEA, 0x96, 0x0F, 0xFF, 0xF6, 0xD1, 0x5D, 0xFF, 0xF5, 0xD1, 0x5A, 0xFF, 0xF4, + 0xD2, 0x58, 0xFF, 0xF3, 0xCE, 0x53, 0xFF, 0xFA, 0xD1, 0x56, 0xFF, 0xE6, 0xB1, 0x3F, 0xFF, 0xC6, 0x64, 0x01, 0xFF, + 0xCE, 0x75, 0x02, 0xFF, 0xD7, 0x87, 0x04, 0xFF, 0xDD, 0x95, 0x02, 0xFF, 0xE4, 0xA4, 0x00, 0xFF, 0xEA, 0xB0, 0x03, + 0xFF, 0xF1, 0xBD, 0x06, 0xFF, 0xF2, 0xC8, 0x1B, 0xFF, 0xFB, 0xD5, 0x42, 0xFF, 0xFB, 0xDD, 0x63, 0xFF, 0xFB, 0xE5, + 0x84, 0xFF, 0xFC, 0xEB, 0x98, 0xFF, 0xFC, 0xF1, 0xAB, 0xFF, 0xFF, 0xF8, 0xBD, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, + 0xFC, 0xCF, 0xFF, 0xFB, 0xF9, 0xCF, 0xFF, 0xFD, 0xFE, 0xD2, 0xFF, 0xFF, 0xFF, 0xD4, 0xFF, 0xFF, 0xF9, 0xC6, 0xFF, + 0xFF, 0xEE, 0xB7, 0xFF, 0xD9, 0xD7, 0x59, 0xFF, 0xE9, 0xB9, 0x40, 0xFF, 0xFF, 0xB9, 0x2E, 0xFF, 0xEF, 0xB1, 0x2B, + 0xFF, 0xEB, 0xAF, 0x27, 0xFF, 0xF1, 0xEF, 0xDD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0xFD, 0xFE, + 0xFF, 0xFF, 0xF9, 0xFD, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xF0, 0xE8, 0xC1, 0xFF, 0xE6, + 0xCD, 0x83, 0xFF, 0xE8, 0xBB, 0x52, 0xFF, 0xEB, 0xA9, 0x21, 0xFF, 0xFF, 0xA1, 0x13, 0xFF, 0xF7, 0x9F, 0x06, 0xFF, + 0xF8, 0x9F, 0x0F, 0xFF, 0xEA, 0xA3, 0x18, 0xFF, 0xE0, 0xB1, 0x43, 0xFF, 0xC9, 0xC2, 0x6D, 0xFF, 0x99, 0xD6, 0xAF, + 0xFF, 0x6A, 0xEB, 0xF1, 0xFF, 0x31, 0xEE, 0xEB, 0xFF, 0x47, 0xE6, 0xF8, 0xFF, 0x3A, 0xE1, 0xFF, 0xFF, 0x41, 0xE1, + 0xFC, 0xFF, 0xF4, 0x98, 0x00, 0xFF, 0xFC, 0xA1, 0x19, 0xFF, 0xF6, 0x9E, 0x15, 0xFF, 0xF1, 0x9A, 0x11, 0xFF, 0xF0, + 0x9A, 0x13, 0xFF, 0xF0, 0x99, 0x14, 0xFF, 0xEE, 0x97, 0x12, 0xFF, 0xEC, 0x95, 0x10, 0xFF, 0xEC, 0x95, 0x10, 0xFF, + 0xF5, 0xCF, 0x5C, 0xFF, 0xF6, 0xD1, 0x5A, 0xFF, 0xF6, 0xD4, 0x59, 0xFF, 0xF2, 0xCD, 0x51, 0xFF, 0xFE, 0xD6, 0x59, + 0xFF, 0xD5, 0x8B, 0x29, 0xFF, 0xCB, 0x6C, 0x02, 0xFF, 0xD1, 0x7A, 0x01, 0xFF, 0xD8, 0x89, 0x01, 0xFF, 0xDE, 0x97, + 0x00, 0xFF, 0xE5, 0xA5, 0x00, 0xFF, 0xEC, 0xB2, 0x00, 0xFF, 0xF3, 0xBE, 0x02, 0xFF, 0xF1, 0xC7, 0x08, 0xFF, 0xFE, + 0xD5, 0x35, 0xFF, 0xFD, 0xDD, 0x58, 0xFF, 0xFB, 0xE4, 0x7C, 0xFF, 0xFC, 0xEA, 0x91, 0xFF, 0xFE, 0xEF, 0xA6, 0xFF, + 0xFE, 0xF2, 0xB0, 0xFF, 0xFE, 0xF5, 0xBA, 0xFF, 0xFC, 0xF5, 0xBD, 0xFF, 0xF9, 0xF5, 0xC0, 0xFF, 0xF6, 0xF7, 0xC0, + 0xFF, 0xF4, 0xF9, 0xC1, 0xFF, 0xFC, 0xFC, 0xC6, 0xFF, 0xFF, 0xFF, 0xCC, 0xFF, 0xF7, 0xF8, 0xC1, 0xFF, 0xF3, 0xCC, + 0x59, 0xFF, 0xF2, 0xB0, 0x38, 0xFF, 0xF5, 0xBA, 0x37, 0xFF, 0xF7, 0xB4, 0x29, 0xFF, 0xF8, 0xFB, 0xFC, 0xFF, 0xFF, + 0xFD, 0xFC, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xF6, 0xFC, 0xFF, 0xFF, 0xF2, 0xFE, 0xFC, 0xFF, + 0xEE, 0xFF, 0xF6, 0xFF, 0xE9, 0xFF, 0xFC, 0xFF, 0xE4, 0xFF, 0xFF, 0xFF, 0xD7, 0xFF, 0xFF, 0xFF, 0xCA, 0xFF, 0xFF, + 0xFF, 0xF1, 0xFB, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFD, 0xFC, 0xFF, 0x88, 0xFF, 0xF6, 0xFF, 0x91, 0xFD, + 0xFB, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0x6C, 0xFC, 0xFC, 0xFF, 0x59, 0xF6, 0xF9, 0xFF, 0x58, 0xEF, 0xF8, 0xFF, 0x57, + 0xE9, 0xF7, 0xFF, 0x59, 0xE3, 0xF6, 0xFF, 0x67, 0xD2, 0xD0, 0xFF, 0xFF, 0x98, 0x08, 0xFF, 0xEF, 0x9A, 0x17, 0xFF, + 0xF1, 0x99, 0x12, 0xFF, 0xF4, 0x98, 0x0C, 0xFF, 0xF3, 0x99, 0x10, 0xFF, 0xF1, 0x99, 0x15, 0xFF, 0xEF, 0x97, 0x13, + 0xFF, 0xED, 0x95, 0x11, 0xFF, 0xED, 0x95, 0x11, 0xFF, 0xF9, 0xD1, 0x5E, 0xFF, 0xF7, 0xD3, 0x5B, 0xFF, 0xF6, 0xD4, + 0x59, 0xFF, 0xF8, 0xD3, 0x57, 0xFF, 0xFF, 0xDA, 0x5E, 0xFF, 0xCD, 0x70, 0x19, 0xFF, 0xCC, 0x6D, 0x02, 0xFF, 0xD2, + 0x7B, 0x03, 0xFF, 0xD9, 0x88, 0x04, 0xFF, 0xDF, 0x96, 0x04, 0xFF, 0xE6, 0xA5, 0x04, 0xFF, 0xE6, 0xAD, 0x01, 0xFF, + 0xE7, 0xB4, 0x00, 0xFF, 0xEA, 0xBE, 0x06, 0xFF, 0xF5, 0xCA, 0x23, 0xFF, 0xF8, 0xD7, 0x4B, 0xFF, 0xFB, 0xE3, 0x74, + 0xFF, 0xFC, 0xE8, 0x89, 0xFF, 0xFE, 0xEC, 0x9E, 0xFF, 0xFE, 0xED, 0xA5, 0xFF, 0xFE, 0xEE, 0xAB, 0xFF, 0xFB, 0xEF, + 0xAD, 0xFF, 0xF9, 0xEF, 0xB0, 0xFF, 0xF8, 0xF2, 0xB3, 0xFF, 0xF8, 0xF5, 0xB6, 0xFF, 0xFC, 0xF8, 0xB5, 0xFF, 0xFF, + 0xFB, 0xB5, 0xFF, 0xFF, 0xF3, 0xD9, 0xFF, 0xF1, 0xB9, 0x1A, 0xFF, 0xF3, 0xB3, 0x28, 0xFF, 0xF6, 0xB3, 0x2A, 0xFF, + 0xF3, 0xCE, 0x73, 0xFF, 0xF5, 0xFD, 0xFD, 0xFF, 0xF9, 0xFE, 0xFD, 0xFF, 0xFE, 0xFF, 0xFD, 0xFF, 0xF8, 0xFE, 0xFF, + 0xFF, 0xF3, 0xFD, 0xFF, 0xFF, 0xEE, 0xFE, 0xFD, 0xFF, 0xE9, 0xFE, 0xFA, 0xFF, 0xE3, 0xFF, 0xFC, 0xFF, 0xDE, 0xFF, + 0xFF, 0xFF, 0xD0, 0xFF, 0xFF, 0xFF, 0xC2, 0xFF, 0xFF, 0xFF, 0xD6, 0xFA, 0xFC, 0xFF, 0xF3, 0xFC, 0xFF, 0xFF, 0xBF, + 0xFF, 0xFE, 0xFF, 0xC4, 0xFA, 0xFC, 0xFF, 0x84, 0xFF, 0xFC, 0xFF, 0x8A, 0xFA, 0xFB, 0xFF, 0x79, 0xF6, 0xFA, 0xFF, + 0x68, 0xF2, 0xF9, 0xFF, 0x5E, 0xED, 0xF6, 0xFF, 0x53, 0xE8, 0xF4, 0xFF, 0x48, 0xE8, 0xF7, 0xFF, 0xA8, 0xBC, 0x87, + 0xFF, 0xFB, 0x9A, 0x10, 0xFF, 0xF1, 0x9B, 0x17, 0xFF, 0xF1, 0x9A, 0x14, 0xFF, 0xF1, 0x9A, 0x10, 0xFF, 0xF2, 0x99, + 0x13, 0xFF, 0xF3, 0x98, 0x16, 0xFF, 0xF1, 0x96, 0x14, 0xFF, 0xEF, 0x94, 0x12, 0xFF, 0xEF, 0x94, 0x12, 0xFF, 0xFC, + 0xD4, 0x61, 0xFF, 0xF9, 0xD4, 0x5D, 0xFF, 0xF6, 0xD4, 0x58, 0xFF, 0xF5, 0xD1, 0x55, 0xFF, 0xF5, 0xCE, 0x53, 0xFF, + 0xBD, 0x4D, 0x01, 0xFF, 0xCD, 0x6E, 0x02, 0xFF, 0xD3, 0x7B, 0x04, 0xFF, 0xDA, 0x87, 0x06, 0xFF, 0xE0, 0x96, 0x09, + 0xFF, 0xE6, 0xA5, 0x0C, 0xFF, 0xE9, 0xB0, 0x0A, 0xFF, 0xEB, 0xBA, 0x09, 0xFF, 0xF3, 0xC5, 0x15, 0xFF, 0xFB, 0xD0, + 0x21, 0xFF, 0xFB, 0xD9, 0x46, 0xFF, 0xFB, 0xE3, 0x6B, 0xFF, 0xFC, 0xE6, 0x81, 0xFF, 0xFD, 0xE9, 0x97, 0xFF, 0xFD, + 0xE8, 0x99, 0xFF, 0xFD, 0xE8, 0x9B, 0xFF, 0xFB, 0xE8, 0x9D, 0xFF, 0xF8, 0xE9, 0x9F, 0xFF, 0xFA, 0xEE, 0xA5, 0xFF, + 0xFC, 0xF2, 0xAB, 0xFF, 0xFB, 0xEF, 0xB0, 0xFF, 0xFB, 0xEB, 0xB4, 0xFF, 0xF8, 0xDD, 0x89, 0xFF, 0xF3, 0xB4, 0x27, + 0xFF, 0xF7, 0xBD, 0x3E, 0xFF, 0xF7, 0xAC, 0x1E, 0xFF, 0xF0, 0xE8, 0xBC, 0xFF, 0xF2, 0xFF, 0xFE, 0xFF, 0xF3, 0xFF, + 0xFD, 0xFF, 0xF4, 0xFE, 0xFD, 0xFF, 0xF2, 0xFE, 0xFD, 0xFF, 0xEF, 0xFE, 0xFE, 0xFF, 0xE9, 0xFE, 0xFE, 0xFF, 0xE3, + 0xFE, 0xFE, 0xFF, 0xDD, 0xFD, 0xFD, 0xFF, 0xD7, 0xFD, 0xFD, 0xFF, 0xC8, 0xFE, 0xFC, 0xFF, 0xB9, 0xFF, 0xFB, 0xFF, + 0x9F, 0xFE, 0xF5, 0xFF, 0xCE, 0xFF, 0xFF, 0xFF, 0xF5, 0xF9, 0xFF, 0xFF, 0xC8, 0xFF, 0xFF, 0xFF, 0xBD, 0xF7, 0xFC, + 0xFF, 0x7A, 0xF8, 0xF8, 0xFF, 0x6B, 0xF5, 0xF8, 0xFF, 0x5C, 0xF3, 0xF9, 0xFF, 0x55, 0xED, 0xF5, 0xFF, 0x4F, 0xE8, + 0xF1, 0xFF, 0x37, 0xEE, 0xF8, 0xFF, 0xE9, 0xA6, 0x3E, 0xFF, 0xF5, 0x9C, 0x17, 0xFF, 0xF4, 0x9D, 0x17, 0xFF, 0xF1, + 0x9C, 0x15, 0xFF, 0xEE, 0x9B, 0x14, 0xFF, 0xF1, 0x99, 0x15, 0xFF, 0xF4, 0x97, 0x17, 0xFF, 0xF2, 0x95, 0x15, 0xFF, + 0xF0, 0x93, 0x13, 0xFF, 0xF0, 0x93, 0x13, 0xFF, 0xFB, 0xD6, 0x66, 0xFF, 0xF4, 0xD1, 0x5E, 0xFF, 0xF6, 0xD3, 0x5F, + 0xFF, 0xF9, 0xD7, 0x59, 0xFF, 0xDA, 0x9D, 0x39, 0xFF, 0xBE, 0x58, 0x08, 0xFF, 0xCD, 0x6C, 0x08, 0xFF, 0xD2, 0x79, + 0x0C, 0xFF, 0xD7, 0x87, 0x0F, 0xFF, 0xDF, 0x96, 0x11, 0xFF, 0xE7, 0xA5, 0x13, 0xFF, 0xEA, 0xB0, 0x13, 0xFF, 0xF5, + 0xC2, 0x1B, 0xFF, 0xF3, 0xC8, 0x0F, 0xFF, 0xF9, 0xD0, 0x16, 0xFF, 0xF4, 0xD2, 0x27, 0xFF, 0xF7, 0xD6, 0x4B, 0xFF, + 0xF8, 0xDA, 0x60, 0xFF, 0xF9, 0xDE, 0x76, 0xFF, 0xF9, 0xDF, 0x7F, 0xFF, 0xFA, 0xE0, 0x87, 0xFF, 0xFB, 0xE4, 0x8C, + 0xFF, 0xFB, 0xE7, 0x91, 0xFF, 0xFC, 0xEA, 0x95, 0xFF, 0xFC, 0xED, 0x9A, 0xFF, 0xFB, 0xEA, 0x9E, 0xFF, 0xFA, 0xE7, + 0xA3, 0xFF, 0xFA, 0xCC, 0x5E, 0xFF, 0xF5, 0xB6, 0x2C, 0xFF, 0xF9, 0xB8, 0x24, 0xFF, 0xF5, 0xB1, 0x14, 0xFF, 0xFF, + 0xFB, 0xFF, 0xFF, 0xEC, 0xFF, 0xFD, 0xFF, 0xED, 0xFF, 0xFF, 0xFF, 0xED, 0xFF, 0xFF, 0xFF, 0xEC, 0xFE, 0xFF, 0xFF, + 0xEA, 0xFD, 0xFD, 0xFF, 0xE3, 0xFD, 0xFD, 0xFF, 0xDC, 0xFD, 0xFD, 0xFF, 0xD5, 0xFD, 0xFD, 0xFF, 0xCE, 0xFD, 0xFD, + 0xFF, 0xC1, 0xFC, 0xFC, 0xFF, 0xB4, 0xFB, 0xFB, 0xFF, 0x8D, 0xFA, 0xF5, 0xFF, 0x89, 0xFC, 0xF8, 0xFF, 0xCB, 0xFA, + 0xF8, 0xFF, 0xF1, 0xFE, 0xF7, 0xFF, 0xBD, 0xFF, 0xF9, 0xFF, 0xC2, 0xF9, 0xFA, 0xFF, 0xAC, 0xF8, 0xFB, 0xFF, 0x96, + 0xF7, 0xFC, 0xFF, 0x91, 0xF4, 0xF9, 0xFF, 0x8C, 0xF0, 0xF7, 0xFF, 0xA8, 0xE4, 0xFF, 0xFF, 0xF6, 0x95, 0x00, 0xFF, + 0xF6, 0x99, 0x07, 0xFF, 0xF7, 0x9D, 0x15, 0xFF, 0xF3, 0x9D, 0x15, 0xFF, 0xF0, 0x9C, 0x15, 0xFF, 0xF2, 0x9A, 0x15, + 0xFF, 0xF4, 0x98, 0x15, 0xFF, 0xF2, 0x97, 0x14, 0xFF, 0xF1, 0x95, 0x12, 0xFF, 0xF1, 0x95, 0x12, 0xFF, 0xFA, 0xD9, + 0x6A, 0xFF, 0xF0, 0xCE, 0x60, 0xFF, 0xF6, 0xD2, 0x66, 0xFF, 0xFD, 0xDD, 0x5C, 0xFF, 0xC0, 0x6C, 0x1E, 0xFF, 0xBE, + 0x63, 0x0E, 0xFF, 0xCD, 0x69, 0x0E, 0xFF, 0xD0, 0x78, 0x13, 0xFF, 0xD4, 0x87, 0x18, 0xFF, 0xDE, 0x96, 0x19, 0xFF, + 0xE9, 0xA6, 0x1A, 0xFF, 0xE3, 0xA8, 0x13, 0xFF, 0xEE, 0xBA, 0x1D, 0xFF, 0xEA, 0xBD, 0x0C, 0xFF, 0xF6, 0xC5, 0x22, + 0xFF, 0xEC, 0xC5, 0x13, 0xFF, 0xF2, 0xCA, 0x2A, 0xFF, 0xF3, 0xCF, 0x40, 0xFF, 0xF4, 0xD3, 0x56, 0xFF, 0xF5, 0xD6, + 0x65, 0xFF, 0xF7, 0xD9, 0x74, 0xFF, 0xFA, 0xDF, 0x7B, 0xFF, 0xFE, 0xE4, 0x82, 0xFF, 0xFD, 0xE6, 0x86, 0xFF, 0xFD, + 0xE7, 0x89, 0xFF, 0xFB, 0xE4, 0x8D, 0xFF, 0xF9, 0xE2, 0x92, 0xFF, 0xFC, 0xBB, 0x33, 0xFF, 0xF6, 0xB9, 0x31, 0xFF, + 0xFD, 0xBA, 0x31, 0xFF, 0xF7, 0xC4, 0x57, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xE6, 0xFF, 0xFD, 0xFF, 0xE6, 0xFF, 0xFF, + 0xFF, 0xE6, 0xFF, 0xFF, 0xFF, 0xE6, 0xFE, 0xFF, 0xFF, 0xE5, 0xFC, 0xFD, 0xFF, 0xDD, 0xFC, 0xFD, 0xFF, 0xD5, 0xFC, + 0xFD, 0xFF, 0xCD, 0xFD, 0xFD, 0xFF, 0xC5, 0xFD, 0xFD, 0xFF, 0xBA, 0xFA, 0xFC, 0xFF, 0xAF, 0xF7, 0xFB, 0xFF, 0x9E, + 0xF9, 0xFE, 0xFF, 0x8D, 0xFB, 0xFF, 0xFF, 0x77, 0xFE, 0xFA, 0xFF, 0x7D, 0xFB, 0xF4, 0xFF, 0xD2, 0xF7, 0xF8, 0xFF, + 0xEE, 0xFF, 0xFD, 0xFF, 0xDF, 0xFD, 0xFE, 0xFF, 0xD0, 0xFB, 0xFE, 0xFF, 0xCD, 0xFA, 0xFD, 0xFF, 0xC9, 0xF9, 0xFC, + 0xFF, 0xCD, 0xD2, 0xA6, 0xFF, 0xEA, 0x98, 0x02, 0xFF, 0xEC, 0xA0, 0x1E, 0xFF, 0xF9, 0x9E, 0x13, 0xFF, 0xF6, 0x9E, + 0x15, 0xFF, 0xF2, 0x9D, 0x16, 0xFF, 0xF2, 0x9B, 0x15, 0xFF, 0xF3, 0x99, 0x14, 0xFF, 0xF2, 0x98, 0x13, 0xFF, 0xF1, + 0x97, 0x12, 0xFF, 0xF1, 0x97, 0x12, 0xFF, 0xF3, 0xD4, 0x55, 0xFF, 0xF0, 0xD1, 0x5B, 0xFF, 0xF6, 0xD6, 0x69, 0xFF, + 0xFF, 0xE2, 0x6D, 0xFF, 0xA7, 0x4F, 0x0B, 0xFF, 0xBE, 0x60, 0x11, 0xFF, 0xCD, 0x6A, 0x0F, 0xFF, 0xD5, 0x83, 0x1F, + 0xFF, 0xDC, 0x89, 0x1E, 0xFF, 0xDD, 0x8B, 0x0F, 0xFF, 0xE0, 0x9B, 0x1A, 0xFF, 0xF3, 0xB0, 0x22, 0xFF, 0xE0, 0xAA, + 0x1D, 0xFF, 0xDF, 0xAE, 0x13, 0xFF, 0xEE, 0xBC, 0x25, 0xFF, 0xE6, 0xB9, 0x14, 0xFF, 0xEF, 0xC1, 0x1F, 0xFF, 0xEF, + 0xC7, 0x25, 0xFF, 0xEE, 0xCD, 0x2B, 0xFF, 0xF0, 0xCD, 0x3C, 0xFF, 0xF3, 0xCE, 0x4E, 0xFF, 0xF8, 0xD6, 0x5B, 0xFF, + 0xFE, 0xDE, 0x68, 0xFF, 0xFC, 0xDD, 0x6D, 0xFF, 0xFA, 0xDC, 0x73, 0xFF, 0xF5, 0xDD, 0x75, 0xFF, 0xF6, 0xD3, 0x70, + 0xFF, 0xFB, 0xBA, 0x30, 0xFF, 0xF5, 0xB8, 0x33, 0xFF, 0xFE, 0xB5, 0x24, 0xFF, 0xE4, 0xDE, 0xA3, 0xFF, 0xDC, 0xFF, + 0xF9, 0xFF, 0xDC, 0xFD, 0xFD, 0xFF, 0xDC, 0xFE, 0xFE, 0xFF, 0xDB, 0xFF, 0xFF, 0xFF, 0xDA, 0xFE, 0xFE, 0xFF, 0xD9, + 0xFD, 0xFC, 0xFF, 0xD2, 0xFD, 0xFC, 0xFF, 0xCA, 0xFD, 0xFC, 0xFF, 0xC3, 0xFD, 0xFD, 0xFF, 0xBB, 0xFD, 0xFD, 0xFF, + 0xAF, 0xFB, 0xFC, 0xFF, 0xA2, 0xFA, 0xFC, 0xFF, 0x92, 0xFA, 0xFD, 0xFF, 0x83, 0xFB, 0xFE, 0xFF, 0x6A, 0xFD, 0xFB, + 0xFF, 0x60, 0xFC, 0xF8, 0xFF, 0x5D, 0xF8, 0xFA, 0xFF, 0x4C, 0xF7, 0xFC, 0xFF, 0x76, 0xF4, 0xFD, 0xFF, 0xA0, 0xF2, + 0xFE, 0xFF, 0x87, 0xEC, 0xF5, 0xFF, 0x5F, 0xE3, 0xF7, 0xFF, 0xB4, 0xBB, 0x50, 0xFF, 0xFE, 0x99, 0x0C, 0xFF, 0xF7, + 0x9E, 0x1A, 0xFF, 0xF6, 0x9D, 0x15, 0xFF, 0xF4, 0x9D, 0x15, 0xFF, 0xF2, 0x9C, 0x15, 0xFF, 0xF2, 0x9B, 0x14, 0xFF, + 0xF1, 0x99, 0x12, 0xFF, 0xF1, 0x99, 0x12, 0xFF, 0xF1, 0x99, 0x12, 0xFF, 0xF1, 0x99, 0x12, 0xFF, 0xFD, 0xD3, 0x66, + 0xFF, 0xF9, 0xD6, 0x69, 0xFF, 0xF5, 0xD9, 0x6B, 0xFF, 0xDC, 0xB6, 0x4E, 0xFF, 0xAE, 0x52, 0x18, 0xFF, 0xC6, 0x66, + 0x1C, 0xFF, 0xBD, 0x5A, 0x00, 0xFF, 0xCA, 0x7D, 0x1A, 0xFF, 0xD4, 0x7B, 0x15, 0xFF, 0xDC, 0x81, 0x04, 0xFF, 0xE7, + 0xA0, 0x2A, 0xFF, 0xD3, 0x88, 0x00, 0xFF, 0xE2, 0xAB, 0x2D, 0xFF, 0xDC, 0xA7, 0x23, 0xFF, 0xE6, 0xB3, 0x29, 0xFF, + 0xE0, 0xAD, 0x16, 0xFF, 0xEB, 0xB7, 0x14, 0xFF, 0xEA, 0xB9, 0x15, 0xFF, 0xE9, 0xBA, 0x16, 0xFF, 0xEC, 0xBE, 0x1F, + 0xFF, 0xEE, 0xC2, 0x28, 0xFF, 0xF6, 0xCD, 0x3B, 0xFF, 0xFE, 0xD8, 0x4E, 0xFF, 0xFB, 0xD5, 0x55, 0xFF, 0xF7, 0xD1, + 0x5D, 0xFF, 0xEF, 0xD6, 0x5D, 0xFF, 0xF3, 0xC4, 0x4E, 0xFF, 0xFA, 0xB9, 0x2E, 0xFF, 0xF4, 0xB8, 0x35, 0xFF, 0xFF, + 0xB1, 0x17, 0xFF, 0xD1, 0xF7, 0xF0, 0xFF, 0xDA, 0xFF, 0xFE, 0xFF, 0xD2, 0xFC, 0xFC, 0xFF, 0xD1, 0xFD, 0xFD, 0xFF, + 0xD0, 0xFE, 0xFD, 0xFF, 0xCF, 0xFD, 0xFC, 0xFF, 0xCD, 0xFD, 0xFB, 0xFF, 0xC6, 0xFD, 0xFC, 0xFF, 0xBF, 0xFD, 0xFC, + 0xFF, 0xB9, 0xFD, 0xFD, 0xFF, 0xB2, 0xFC, 0xFD, 0xFF, 0xA3, 0xFC, 0xFD, 0xFF, 0x95, 0xFC, 0xFD, 0xFF, 0x87, 0xFC, + 0xFC, 0xFF, 0x78, 0xFB, 0xFC, 0xFF, 0x6C, 0xFA, 0xFD, 0xFF, 0x5F, 0xF8, 0xFD, 0xFF, 0x45, 0xF6, 0xF9, 0xFF, 0x47, + 0xEF, 0xF5, 0xFF, 0x37, 0xE9, 0xF2, 0xFF, 0x28, 0xE4, 0xEE, 0xFF, 0x24, 0xE3, 0xED, 0xFF, 0x05, 0xDD, 0xFF, 0xFF, + 0xFF, 0x99, 0x03, 0xFF, 0xF5, 0xA0, 0x16, 0xFF, 0xF4, 0x9E, 0x16, 0xFF, 0xF3, 0x9C, 0x16, 0xFF, 0xF2, 0x9C, 0x15, + 0xFF, 0xF2, 0x9B, 0x14, 0xFF, 0xF1, 0x9A, 0x12, 0xFF, 0xEF, 0x99, 0x10, 0xFF, 0xF0, 0x9A, 0x11, 0xFF, 0xF1, 0x9B, + 0x12, 0xFF, 0xF1, 0x9B, 0x12, 0xFF, 0xFB, 0xD5, 0x65, 0xFF, 0xFC, 0xD4, 0x70, 0xFF, 0xFF, 0xE2, 0x77, 0xFF, 0xC7, + 0x86, 0x3B, 0xFF, 0xBA, 0x5F, 0x23, 0xFF, 0xBA, 0x6A, 0x1E, 0xFF, 0xD0, 0x7A, 0x21, 0xFF, 0xD7, 0x87, 0x27, 0xFF, + 0xD6, 0x8C, 0x24, 0xFF, 0xD3, 0x8D, 0x1D, 0xFF, 0xD0, 0x88, 0x21, 0xFF, 0xEA, 0xA0, 0x2B, 0xFF, 0xD5, 0x95, 0x21, + 0xFF, 0xEE, 0xA9, 0x30, 0xFF, 0xDA, 0xA0, 0x20, 0xFF, 0xDD, 0xA1, 0x16, 0xFF, 0xDF, 0xA1, 0x0D, 0xFF, 0xE2, 0xAB, + 0x19, 0xFF, 0xEB, 0xB1, 0x12, 0xFF, 0xED, 0xB8, 0x0F, 0xFF, 0xEE, 0xBF, 0x0C, 0xFF, 0xEF, 0xC1, 0x1C, 0xFF, 0xF0, + 0xC3, 0x2C, 0xFF, 0xF1, 0xC4, 0x36, 0xFF, 0xF3, 0xC5, 0x40, 0xFF, 0xF1, 0xC9, 0x46, 0xFF, 0xF6, 0xC2, 0x45, 0xFF, + 0xF9, 0xBA, 0x31, 0xFF, 0xF6, 0xB7, 0x30, 0xFF, 0xF4, 0xC1, 0x4B, 0xFF, 0xC0, 0xFA, 0xF5, 0xFF, 0xC6, 0xFF, 0xFD, + 0xFF, 0xC4, 0xFC, 0xFC, 0xFF, 0xC4, 0xFC, 0xFD, 0xFF, 0xC3, 0xFD, 0xFD, 0xFF, 0xC2, 0xFD, 0xFC, 0xFF, 0xC1, 0xFC, + 0xFB, 0xFF, 0xB5, 0xF8, 0xF8, 0xFF, 0xB2, 0xFD, 0xFC, 0xFF, 0xAA, 0xFC, 0xFC, 0xFF, 0xA3, 0xFC, 0xFC, 0xFF, 0x95, + 0xFB, 0xFC, 0xFF, 0x88, 0xFB, 0xFB, 0xFF, 0x7A, 0xFB, 0xFB, 0xFF, 0x6D, 0xFA, 0xFB, 0xFF, 0x61, 0xF8, 0xFB, 0xFF, + 0x56, 0xF6, 0xFC, 0xFF, 0x44, 0xF2, 0xF8, 0xFF, 0x40, 0xEA, 0xF4, 0xFF, 0x31, 0xE5, 0xEF, 0xFF, 0x23, 0xDF, 0xEA, + 0xFF, 0x1C, 0xE0, 0xFA, 0xFF, 0x44, 0xD1, 0xC5, 0xFF, 0xFE, 0xA1, 0x0A, 0xFF, 0xF9, 0x9F, 0x15, 0xFF, 0xF5, 0x9F, + 0x17, 0xFF, 0xF2, 0x9F, 0x18, 0xFF, 0xF2, 0x9E, 0x16, 0xFF, 0xF2, 0x9D, 0x15, 0xFF, 0xF5, 0x9F, 0x16, 0xFF, 0xF8, + 0xA0, 0x18, 0xFF, 0xF5, 0x9D, 0x15, 0xFF, 0xF2, 0x9A, 0x12, 0xFF, 0xF2, 0x9A, 0x12, 0xFF, 0xF9, 0xD7, 0x64, 0xFF, + 0xF6, 0xD1, 0x64, 0xFF, 0xFF, 0xE6, 0x5D, 0xFF, 0x9A, 0x43, 0x03, 0xFF, 0xA5, 0x4B, 0x0D, 0xFF, 0xCC, 0x7B, 0x30, + 0xFF, 0xC0, 0x54, 0x04, 0xFF, 0xC9, 0x53, 0x00, 0xFF, 0xC5, 0x67, 0x03, 0xFF, 0xC9, 0x87, 0x25, 0xFF, 0xCA, 0x80, + 0x28, 0xFF, 0xD0, 0x88, 0x27, 0xFF, 0xD7, 0x90, 0x26, 0xFF, 0xC9, 0x74, 0x06, 0xFF, 0xCF, 0x8D, 0x17, 0xFF, 0xE1, + 0x9C, 0x1E, 0xFF, 0xE3, 0x9B, 0x16, 0xFF, 0xDA, 0x9E, 0x1E, 0xFF, 0xDD, 0x97, 0x00, 0xFF, 0xE6, 0xA4, 0x03, 0xFF, + 0xEE, 0xB1, 0x07, 0xFF, 0xE8, 0xB0, 0x08, 0xFF, 0xE2, 0xAE, 0x09, 0xFF, 0xE8, 0xB4, 0x16, 0xFF, 0xEF, 0xB9, 0x23, + 0xFF, 0xF4, 0xBD, 0x30, 0xFF, 0xF9, 0xC1, 0x3C, 0xFF, 0xF9, 0xBB, 0x34, 0xFF, 0xF9, 0xB6, 0x2C, 0xFF, 0xE8, 0xD2, + 0x80, 0xFF, 0xAE, 0xFD, 0xFA, 0xFF, 0xB2, 0xFC, 0xFB, 0xFF, 0xB6, 0xFB, 0xFC, 0xFF, 0xB6, 0xFC, 0xFC, 0xFF, 0xB6, + 0xFC, 0xFD, 0xFF, 0xB5, 0xFC, 0xFC, 0xFF, 0xB4, 0xFC, 0xFB, 0xFF, 0xA4, 0xF4, 0xF3, 0xFF, 0xA5, 0xFC, 0xFC, 0xFF, + 0x9C, 0xFC, 0xFC, 0xFF, 0x93, 0xFB, 0xFC, 0xFF, 0x87, 0xFB, 0xFB, 0xFF, 0x7A, 0xFA, 0xFA, 0xFF, 0x6D, 0xFA, 0xFA, + 0xFF, 0x61, 0xF9, 0xF9, 0xFF, 0x57, 0xF7, 0xFA, 0xFF, 0x4E, 0xF4, 0xFA, 0xFF, 0x44, 0xED, 0xF6, 0xFF, 0x3A, 0xE6, + 0xF3, 0xFF, 0x2C, 0xE1, 0xED, 0xFF, 0x1E, 0xDB, 0xE7, 0xFF, 0x19, 0xD1, 0xFF, 0xFF, 0x8F, 0xB0, 0x77, 0xFF, 0xFD, + 0xA0, 0x09, 0xFF, 0xFD, 0x9D, 0x14, 0xFF, 0xF7, 0x9F, 0x17, 0xFF, 0xF2, 0xA2, 0x1A, 0xFF, 0xF2, 0xA0, 0x18, 0xFF, + 0xF2, 0x9E, 0x16, 0xFF, 0xF1, 0x9B, 0x13, 0xFF, 0xF0, 0x98, 0x10, 0xFF, 0xF1, 0x99, 0x11, 0xFF, 0xF2, 0x9A, 0x12, + 0xFF, 0xF2, 0x9A, 0x12, 0xFF, 0xF7, 0xD4, 0x5F, 0xFF, 0xFC, 0xDC, 0x67, 0xFF, 0xF0, 0xC1, 0x4F, 0xFF, 0x8A, 0x2B, + 0x00, 0xFF, 0xBF, 0x6A, 0x2D, 0xFF, 0xAC, 0x47, 0x05, 0xFF, 0xB9, 0x43, 0x00, 0xFF, 0xC4, 0x85, 0x35, 0xFF, 0xBB, + 0x4D, 0x06, 0xFF, 0xC3, 0x61, 0x13, 0xFF, 0xCA, 0x70, 0x2C, 0xFF, 0xB3, 0x5A, 0x0F, 0xFF, 0xCC, 0x74, 0x21, 0xFF, + 0xC2, 0x69, 0x11, 0xFF, 0xC2, 0x78, 0x18, 0xFF, 0xD0, 0x80, 0x1C, 0xFF, 0xD6, 0x7F, 0x18, 0xFF, 0xD3, 0x86, 0x1A, + 0xFF, 0xDD, 0x8F, 0x10, 0xFF, 0xDA, 0x8C, 0x02, 0xFF, 0xE6, 0x99, 0x04, 0xFF, 0xE1, 0x9B, 0x04, 0xFF, 0xDC, 0x9D, + 0x04, 0xFF, 0xE1, 0xA6, 0x05, 0xFF, 0xDD, 0xA6, 0x00, 0xFF, 0xEE, 0xB6, 0x1F, 0xFF, 0xF6, 0xBD, 0x39, 0xFF, 0xF6, + 0xBB, 0x38, 0xFF, 0xFC, 0xB5, 0x24, 0xFF, 0xB8, 0xE8, 0xBF, 0xFF, 0xA2, 0xFE, 0xFA, 0xFF, 0xA5, 0xFC, 0xFB, 0xFF, + 0xA8, 0xFA, 0xFB, 0xFF, 0xA7, 0xFB, 0xFC, 0xFF, 0xA6, 0xFC, 0xFC, 0xFF, 0xA2, 0xFB, 0xFA, 0xFF, 0x9F, 0xFA, 0xF8, + 0xFF, 0x94, 0xF7, 0xF5, 0xFF, 0x92, 0xFB, 0xFA, 0xFF, 0x8B, 0xFB, 0xFA, 0xFF, 0x84, 0xFB, 0xFB, 0xFF, 0x78, 0xFA, + 0xFA, 0xFF, 0x6D, 0xF9, 0xF9, 0xFF, 0x61, 0xF9, 0xF9, 0xFF, 0x55, 0xF8, 0xF8, 0xFF, 0x4B, 0xF6, 0xF8, 0xFF, 0x41, + 0xF3, 0xF9, 0xFF, 0x39, 0xEC, 0xF5, 0xFF, 0x30, 0xE4, 0xF1, 0xFF, 0x28, 0xDD, 0xEE, 0xFF, 0x1F, 0xD6, 0xEB, 0xFF, + 0x00, 0xD9, 0xEE, 0xFF, 0xE4, 0xA6, 0x32, 0xFF, 0xFF, 0xA4, 0x18, 0xFF, 0xF3, 0xA4, 0x28, 0xFF, 0xF4, 0xA2, 0x20, + 0xFF, 0xF4, 0xA0, 0x18, 0xFF, 0xF4, 0x9E, 0x16, 0xFF, 0xF3, 0x9D, 0x15, 0xFF, 0xF2, 0x9B, 0x13, 0xFF, 0xF2, 0x99, + 0x11, 0xFF, 0xF2, 0x99, 0x11, 0xFF, 0xF3, 0x9A, 0x12, 0xFF, 0xF3, 0x9A, 0x12, 0xFF, 0xF5, 0xD1, 0x5B, 0xFF, 0xFA, + 0xDF, 0x62, 0xFF, 0xCC, 0x8C, 0x30, 0xFF, 0x91, 0x2C, 0x05, 0xFF, 0x9A, 0x49, 0x0E, 0xFF, 0x9E, 0x36, 0x00, 0xFF, + 0x96, 0x38, 0x00, 0xFF, 0xB6, 0x5E, 0x14, 0xFF, 0xD9, 0xAA, 0x53, 0xFF, 0xE2, 0xA6, 0x30, 0xFF, 0xEE, 0xBB, 0x44, + 0xFF, 0xFF, 0xDD, 0x6D, 0xFF, 0xF9, 0xDE, 0x76, 0xFF, 0xF9, 0xD9, 0x6C, 0xFF, 0xF8, 0xD4, 0x63, 0xFF, 0xF3, 0xC4, + 0x54, 0xFF, 0xED, 0xB4, 0x44, 0xFF, 0xD5, 0x8E, 0x23, 0xFF, 0xCE, 0x77, 0x11, 0xFF, 0xC6, 0x6C, 0x00, 0xFF, 0xDE, + 0x81, 0x02, 0xFF, 0xDA, 0x87, 0x00, 0xFF, 0xD6, 0x8D, 0x00, 0xFF, 0xE1, 0x9B, 0x06, 0xFF, 0xDC, 0x98, 0x00, 0xFF, + 0xF0, 0xB1, 0x22, 0xFF, 0xF4, 0xB9, 0x35, 0xFF, 0xF3, 0xBC, 0x3C, 0xFF, 0xFF, 0xB4, 0x1B, 0xFF, 0x89, 0xFD, 0xFE, + 0xFF, 0x95, 0xFF, 0xFA, 0xFF, 0x97, 0xFC, 0xFA, 0xFF, 0x99, 0xF8, 0xFB, 0xFF, 0x97, 0xFB, 0xFB, 0xFF, 0x95, 0xFD, + 0xFC, 0xFF, 0x8F, 0xFB, 0xF9, 0xFF, 0x89, 0xF9, 0xF6, 0xFF, 0x84, 0xF9, 0xF7, 0xFF, 0x7F, 0xF9, 0xF8, 0xFF, 0x7A, + 0xFA, 0xF9, 0xFF, 0x75, 0xFA, 0xFA, 0xFF, 0x6A, 0xF9, 0xF9, 0xFF, 0x5F, 0xF9, 0xF8, 0xFF, 0x54, 0xF8, 0xF7, 0xFF, + 0x49, 0xF7, 0xF6, 0xFF, 0x3F, 0xF5, 0xF7, 0xFF, 0x35, 0xF2, 0xF7, 0xFF, 0x2E, 0xEB, 0xF3, 0xFF, 0x27, 0xE3, 0xF0, + 0xFF, 0x24, 0xDA, 0xF0, 0xFF, 0x21, 0xD1, 0xF0, 0xFF, 0x23, 0xC9, 0xE8, 0xFF, 0xFF, 0x9B, 0x03, 0xFF, 0xF6, 0xA3, + 0x20, 0xFF, 0xF6, 0xA1, 0x16, 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9D, 0x16, 0xFF, 0xF6, 0x9C, 0x15, 0xFF, 0xF5, + 0x9B, 0x14, 0xFF, 0xF4, 0x9A, 0x13, 0xFF, 0xF3, 0x99, 0x12, 0xFF, 0xF3, 0x99, 0x12, 0xFF, 0xF3, 0x99, 0x12, 0xFF, + 0xF3, 0x99, 0x12, 0xFF, 0xFE, 0xE2, 0x5A, 0xFF, 0xFF, 0xD7, 0x64, 0xFF, 0x97, 0x46, 0x0C, 0xFF, 0x82, 0x25, 0x00, + 0xFF, 0xB7, 0x6A, 0x1D, 0xFF, 0xDE, 0xA2, 0x39, 0xFF, 0xFF, 0xE5, 0x5E, 0xFF, 0xFD, 0xD8, 0x51, 0xFF, 0xF5, 0xD6, + 0x4C, 0xFF, 0xF4, 0xCC, 0x48, 0xFF, 0xF6, 0xCF, 0x5E, 0xFF, 0xFE, 0xD9, 0x67, 0xFF, 0xF7, 0xD3, 0x61, 0xFF, 0xF8, + 0xD1, 0x5A, 0xFF, 0xFE, 0xCB, 0x41, 0xFF, 0xFE, 0xCE, 0x53, 0xFF, 0xF5, 0xCF, 0x51, 0xFF, 0xF6, 0xCA, 0x49, 0xFF, + 0xFF, 0xCD, 0x49, 0xFF, 0xFF, 0xB9, 0x3F, 0xFF, 0xDA, 0x7E, 0x0E, 0xFF, 0xC2, 0x69, 0x00, 0xFF, 0xDA, 0x84, 0x05, + 0xFF, 0xD5, 0x84, 0x01, 0xFF, 0xD8, 0x8C, 0x05, 0xFF, 0xF8, 0xBE, 0x37, 0xFF, 0xF6, 0xBE, 0x3A, 0xFF, 0xFF, 0xBD, + 0x34, 0xFF, 0xE1, 0xC6, 0x61, 0xFF, 0x79, 0xF3, 0xFB, 0xFF, 0x82, 0xFA, 0xF7, 0xFF, 0x83, 0xF9, 0xF9, 0xFF, 0x83, + 0xF7, 0xFA, 0xFF, 0x7F, 0xF7, 0xF8, 0xFF, 0x7B, 0xF6, 0xF6, 0xFF, 0x79, 0xF8, 0xF7, 0xFF, 0x77, 0xFA, 0xF8, 0xFF, + 0x71, 0xF9, 0xF7, 0xFF, 0x6C, 0xF8, 0xF7, 0xFF, 0x6B, 0xFC, 0xFB, 0xFF, 0x63, 0xF8, 0xF8, 0xFF, 0x5A, 0xF7, 0xF8, + 0xFF, 0x52, 0xF7, 0xF7, 0xFF, 0x48, 0xF5, 0xF7, 0xFF, 0x3F, 0xF4, 0xF6, 0xFF, 0x37, 0xF2, 0xF5, 0xFF, 0x2F, 0xEF, + 0xF4, 0xFF, 0x27, 0xE6, 0xF1, 0xFF, 0x20, 0xDD, 0xEE, 0xFF, 0x1F, 0xD6, 0xEA, 0xFF, 0x10, 0xCC, 0xF1, 0xFF, 0x6C, + 0xB9, 0x9D, 0xFF, 0xFE, 0x9F, 0x0B, 0xFF, 0xF8, 0xA3, 0x1A, 0xFF, 0xF9, 0xA2, 0x16, 0xFF, 0xF8, 0xA0, 0x16, 0xFF, + 0xF7, 0x9E, 0x16, 0xFF, 0xF7, 0x9D, 0x15, 0xFF, 0xF6, 0x9B, 0x14, 0xFF, 0xF5, 0x9A, 0x14, 0xFF, 0xF4, 0x99, 0x13, + 0xFF, 0xF4, 0x99, 0x13, 0xFF, 0xF4, 0x99, 0x13, 0xFF, 0xF4, 0x99, 0x13, 0xFF, 0xF8, 0xD8, 0x60, 0xFF, 0xF7, 0xD8, + 0x5A, 0xFF, 0xD7, 0xAD, 0x4B, 0xFF, 0xFF, 0xDD, 0x68, 0xFF, 0xF7, 0xDC, 0x55, 0xFF, 0xFC, 0xD6, 0x55, 0xFF, 0xFF, + 0xCF, 0x54, 0xFF, 0xFF, 0xD5, 0x5C, 0xFF, 0xF1, 0xCA, 0x53, 0xFF, 0xF5, 0xCA, 0x4A, 0xFF, 0xF9, 0xC9, 0x42, 0xFF, + 0xF7, 0xC9, 0x47, 0xFF, 0xF5, 0xC8, 0x4B, 0xFF, 0xF0, 0xCF, 0x5C, 0xFF, 0xF8, 0xCC, 0x46, 0xFF, 0xFF, 0xCA, 0x55, + 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xFB, 0xC2, 0x43, 0xFF, 0xFC, 0xC1, 0x48, 0xFF, 0xF3, 0xBE, 0x3E, 0xFF, 0xFA, 0xCB, + 0x43, 0xFF, 0xFC, 0xB3, 0x37, 0xFF, 0xDD, 0x7B, 0x0B, 0xFF, 0xC8, 0x6D, 0x00, 0xFF, 0xD4, 0x7F, 0x0D, 0xFF, 0xFF, + 0xCC, 0x4D, 0xFF, 0xF9, 0xC2, 0x3E, 0xFF, 0xFF, 0xC1, 0x2D, 0xFF, 0xA7, 0xDE, 0xA7, 0xFF, 0x5B, 0xEB, 0xF7, 0xFF, + 0x6F, 0xF5, 0xF4, 0xFF, 0x6E, 0xF5, 0xF7, 0xFF, 0x6D, 0xF6, 0xF9, 0xFF, 0x67, 0xF3, 0xF5, 0xFF, 0x60, 0xF0, 0xF1, + 0xFF, 0x62, 0xF6, 0xF5, 0xFF, 0x65, 0xFC, 0xFA, 0xFF, 0x5E, 0xF9, 0xF8, 0xFF, 0x58, 0xF6, 0xF5, 0xFF, 0x5D, 0xFE, + 0xFE, 0xFF, 0x52, 0xF6, 0xF6, 0xFF, 0x4B, 0xF5, 0xF6, 0xFF, 0x44, 0xF5, 0xF7, 0xFF, 0x3D, 0xF3, 0xF6, 0xFF, 0x35, + 0xF1, 0xF5, 0xFF, 0x2F, 0xEE, 0xF3, 0xFF, 0x28, 0xEB, 0xF0, 0xFF, 0x20, 0xE1, 0xEE, 0xFF, 0x18, 0xD8, 0xEC, 0xFF, + 0x1A, 0xD2, 0xE4, 0xFF, 0x00, 0xC6, 0xF3, 0xFF, 0xB4, 0xA8, 0x51, 0xFF, 0xFA, 0xA3, 0x13, 0xFF, 0xFB, 0xA3, 0x15, + 0xFF, 0xFB, 0xA3, 0x17, 0xFF, 0xFA, 0xA0, 0x16, 0xFF, 0xF8, 0x9E, 0x16, 0xFF, 0xF7, 0x9D, 0x15, 0xFF, 0xF7, 0x9C, + 0x15, 0xFF, 0xF6, 0x9A, 0x14, 0xFF, 0xF6, 0x99, 0x14, 0xFF, 0xF6, 0x99, 0x14, 0xFF, 0xF6, 0x99, 0x14, 0xFF, 0xF6, + 0x99, 0x14, 0xFF, 0xF1, 0xCE, 0x58, 0xFF, 0xFD, 0xDC, 0x59, 0xFF, 0xF8, 0xD5, 0x55, 0xFF, 0xFF, 0xDD, 0x5D, 0xFF, + 0xF3, 0xCE, 0x4D, 0xFF, 0xF3, 0xCB, 0x4C, 0xFF, 0xF3, 0xC8, 0x4C, 0xFF, 0xFB, 0xD1, 0x56, 0xFF, 0xFC, 0xD3, 0x58, + 0xFF, 0xFB, 0xCE, 0x4F, 0xFF, 0xFA, 0xC9, 0x47, 0xFF, 0xF9, 0xC8, 0x48, 0xFF, 0xF8, 0xC7, 0x49, 0xFF, 0xF5, 0xCA, + 0x50, 0xFF, 0xF9, 0xC9, 0x44, 0xFF, 0xFD, 0xC8, 0x4B, 0xFF, 0xF9, 0xC5, 0x3E, 0xFF, 0xFA, 0xC3, 0x40, 0xFF, 0xFA, + 0xC2, 0x43, 0xFF, 0xF3, 0xBD, 0x3A, 0xFF, 0xF3, 0xBF, 0x3A, 0xFF, 0xFC, 0xC7, 0x3E, 0xFF, 0xFC, 0xC6, 0x3A, 0xFF, + 0xE2, 0xA1, 0x24, 0xFF, 0xD9, 0x8C, 0x1F, 0xFF, 0xF6, 0xB9, 0x36, 0xFF, 0xFA, 0xBB, 0x26, 0xFF, 0xF3, 0xBA, 0x29, + 0xFF, 0x56, 0xD7, 0xCD, 0xFF, 0x5A, 0xFA, 0xF9, 0xFF, 0x48, 0xDA, 0xD9, 0xFF, 0x58, 0xEC, 0xED, 0xFF, 0x5F, 0xF5, + 0xF9, 0xFF, 0x4D, 0xEF, 0xF1, 0xFF, 0x3A, 0xE9, 0xE9, 0xFF, 0x45, 0xEE, 0xED, 0xFF, 0x50, 0xF4, 0xF2, 0xFF, 0x4E, + 0xF3, 0xF9, 0xFF, 0x44, 0xF0, 0xED, 0xFF, 0x4B, 0xF8, 0xFE, 0xFF, 0x41, 0xF5, 0xF4, 0xFF, 0x3C, 0xF4, 0xF5, 0xFF, + 0x37, 0xF2, 0xF6, 0xFF, 0x31, 0xF0, 0xF5, 0xFF, 0x2A, 0xEF, 0xF4, 0xFF, 0x26, 0xEA, 0xF2, 0xFF, 0x22, 0xE6, 0xF0, + 0xFF, 0x1C, 0xDB, 0xEE, 0xFF, 0x17, 0xD0, 0xEC, 0xFF, 0x08, 0xCC, 0xF0, 0xFF, 0x08, 0xC4, 0xF5, 0xFF, 0xFF, 0xAD, + 0x0E, 0xFF, 0xF9, 0xA1, 0x16, 0xFF, 0xF8, 0xA1, 0x17, 0xFF, 0xF8, 0xA1, 0x18, 0xFF, 0xF8, 0xA0, 0x17, 0xFF, 0xF8, + 0x9E, 0x16, 0xFF, 0xF8, 0x9D, 0x16, 0xFF, 0xF8, 0x9C, 0x15, 0xFF, 0xF7, 0x9A, 0x15, 0xFF, 0xF7, 0x99, 0x14, 0xFF, + 0xF7, 0x99, 0x14, 0xFF, 0xF7, 0x99, 0x14, 0xFF, 0xF7, 0x99, 0x14, 0xFF, 0xFB, 0xD5, 0x60, 0xFF, 0xFA, 0xD3, 0x5A, + 0xFF, 0xFA, 0xD1, 0x55, 0xFF, 0xFC, 0xD0, 0x55, 0xFF, 0xFE, 0xCF, 0x54, 0xFF, 0xFA, 0xD0, 0x54, 0xFF, 0xF6, 0xD1, + 0x53, 0xFF, 0xF6, 0xCE, 0x50, 0xFF, 0xF7, 0xCB, 0x4E, 0xFF, 0xF9, 0xCA, 0x4C, 0xFF, 0xFA, 0xCA, 0x4B, 0xFF, 0xFB, + 0xC8, 0x49, 0xFF, 0xFB, 0xC6, 0x47, 0xFF, 0xFB, 0xC6, 0x45, 0xFF, 0xFA, 0xC6, 0x43, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, + 0xF8, 0xC6, 0x3F, 0xFF, 0xF8, 0xC4, 0x3E, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xFB, 0xC3, 0x3F, 0xFF, 0xFD, 0xC3, 0x40, + 0xFF, 0xF2, 0xBA, 0x38, 0xFF, 0xF7, 0xC0, 0x3F, 0xFF, 0xFA, 0xC2, 0x3D, 0xFF, 0xFD, 0xC5, 0x3A, 0xFF, 0xF6, 0xC1, + 0x37, 0xFF, 0xEF, 0xBD, 0x34, 0xFF, 0xEF, 0xBB, 0x2D, 0xFF, 0x21, 0xD6, 0xDD, 0xFF, 0x37, 0xDC, 0xBF, 0xFF, 0x41, + 0xE0, 0xDD, 0xFF, 0x49, 0xEA, 0xEB, 0xFF, 0x41, 0xE3, 0xEA, 0xFF, 0x41, 0xE8, 0xED, 0xFF, 0x41, 0xED, 0xF1, 0xFF, + 0x3F, 0xEC, 0xED, 0xFF, 0x3C, 0xEB, 0xEA, 0xFF, 0x3E, 0xEE, 0xFA, 0xFF, 0x31, 0xEB, 0xE5, 0xFF, 0x39, 0xF2, 0xFE, + 0xFF, 0x31, 0xF4, 0xF1, 0xFF, 0x2D, 0xF2, 0xF3, 0xFF, 0x29, 0xF0, 0xF5, 0xFF, 0x25, 0xEE, 0xF4, 0xFF, 0x20, 0xEC, + 0xF4, 0xFF, 0x1E, 0xE6, 0xF1, 0xFF, 0x1C, 0xE1, 0xEF, 0xFF, 0x19, 0xD5, 0xED, 0xFF, 0x16, 0xC9, 0xEB, 0xFF, 0x0B, + 0xC3, 0xDE, 0xFF, 0x39, 0xBE, 0xBA, 0xFF, 0xF8, 0x98, 0x07, 0xFF, 0xF8, 0x9F, 0x19, 0xFF, 0xF6, 0x9F, 0x19, 0xFF, + 0xF5, 0x9F, 0x19, 0xFF, 0xF7, 0x9F, 0x18, 0xFF, 0xF9, 0x9F, 0x16, 0xFF, 0xF9, 0x9D, 0x16, 0xFF, 0xF9, 0x9C, 0x16, + 0xFF, 0xF9, 0x9A, 0x16, 0xFF, 0xF8, 0x99, 0x15, 0xFF, 0xF8, 0x99, 0x15, 0xFF, 0xF8, 0x99, 0x15, 0xFF, 0xF8, 0x99, + 0x15, 0xFF, 0xF8, 0xD4, 0x5C, 0xFF, 0xF8, 0xD4, 0x58, 0xFF, 0xF8, 0xD3, 0x54, 0xFF, 0xF9, 0xD1, 0x56, 0xFF, 0xFA, + 0xD0, 0x57, 0xFF, 0xF8, 0xD0, 0x55, 0xFF, 0xF5, 0xD0, 0x53, 0xFF, 0xF7, 0xCE, 0x50, 0xFF, 0xF9, 0xCC, 0x4D, 0xFF, + 0xF9, 0xCB, 0x4C, 0xFF, 0xFA, 0xCA, 0x4A, 0xFF, 0xFB, 0xC8, 0x48, 0xFF, 0xFB, 0xC7, 0x46, 0xFF, 0xFA, 0xC6, 0x44, + 0xFF, 0xFA, 0xC6, 0x43, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, 0xC6, 0x3F, 0xFF, 0xF9, 0xC4, 0x3E, 0xFF, 0xF9, 0xC3, + 0x3D, 0xFF, 0xFA, 0xC2, 0x3E, 0xFF, 0xFB, 0xC1, 0x3E, 0xFF, 0xF5, 0xBD, 0x3A, 0xFF, 0xF7, 0xC1, 0x3D, 0xFF, 0xF8, + 0xC0, 0x3A, 0xFF, 0xF9, 0xC0, 0x37, 0xFF, 0xFF, 0xBD, 0x36, 0xFF, 0xFF, 0xBB, 0x35, 0xFF, 0x84, 0xBA, 0x66, 0xFF, + 0x18, 0xD2, 0xAF, 0xFF, 0x19, 0xD2, 0xB3, 0xFF, 0x39, 0xDA, 0xD2, 0xFF, 0x3D, 0xDC, 0xE1, 0xFF, 0x31, 0xD4, 0xD5, + 0xFF, 0x37, 0xDF, 0xE1, 0xFF, 0x3E, 0xE9, 0xEC, 0xFF, 0x35, 0xE6, 0xE1, 0xFF, 0x35, 0xE5, 0xE9, 0xFF, 0x34, 0xE5, + 0xF0, 0xFF, 0x2A, 0xE3, 0xE4, 0xFF, 0x2D, 0xE5, 0xF5, 0xFF, 0x28, 0xEB, 0xE8, 0xFF, 0x2A, 0xEE, 0xF0, 0xFF, 0x24, + 0xE8, 0xEF, 0xFF, 0x20, 0xE4, 0xEC, 0xFF, 0x1C, 0xDF, 0xE9, 0xFF, 0x1C, 0xDB, 0xEB, 0xFF, 0x1B, 0xD7, 0xED, 0xFF, + 0x18, 0xCE, 0xE9, 0xFF, 0x15, 0xC5, 0xE5, 0xFF, 0x03, 0xBF, 0xE7, 0xFF, 0x92, 0xB1, 0x6C, 0xFF, 0xFB, 0x9C, 0x10, + 0xFF, 0xF7, 0xA0, 0x17, 0xFF, 0xF5, 0xA0, 0x19, 0xFF, 0xF3, 0xA0, 0x1B, 0xFF, 0xF6, 0x9F, 0x19, 0xFF, 0xF9, 0x9F, + 0x16, 0xFF, 0xF8, 0x9E, 0x16, 0xFF, 0xF8, 0x9C, 0x15, 0xFF, 0xF8, 0x9B, 0x15, 0xFF, 0xF8, 0x99, 0x15, 0xFF, 0xF7, + 0x99, 0x14, 0xFF, 0xF7, 0x98, 0x14, 0xFF, 0xF7, 0x98, 0x14, 0xFF, 0xF6, 0xD3, 0x57, 0xFF, 0xF6, 0xD4, 0x55, 0xFF, + 0xF6, 0xD5, 0x53, 0xFF, 0xF7, 0xD2, 0x57, 0xFF, 0xF7, 0xD0, 0x5B, 0xFF, 0xF6, 0xD0, 0x57, 0xFF, 0xF5, 0xCF, 0x54, + 0xFF, 0xF7, 0xCE, 0x50, 0xFF, 0xFA, 0xCC, 0x4C, 0xFF, 0xFA, 0xCB, 0x4B, 0xFF, 0xFA, 0xCA, 0x49, 0xFF, 0xFA, 0xC8, + 0x47, 0xFF, 0xFB, 0xC7, 0x46, 0xFF, 0xFA, 0xC7, 0x44, 0xFF, 0xFA, 0xC6, 0x43, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, + 0xC5, 0x3F, 0xFF, 0xF9, 0xC4, 0x3E, 0xFF, 0xF9, 0xC2, 0x3D, 0xFF, 0xF9, 0xC1, 0x3C, 0xFF, 0xF9, 0xC0, 0x3B, 0xFF, + 0xF8, 0xC1, 0x3C, 0xFF, 0xF7, 0xC2, 0x3C, 0xFF, 0xF6, 0xBE, 0x38, 0xFF, 0xF5, 0xBB, 0x34, 0xFF, 0xFD, 0xBC, 0x35, + 0xFF, 0xFF, 0xBE, 0x36, 0xFF, 0xFB, 0xBB, 0x45, 0xFF, 0x2B, 0xC9, 0x82, 0xFF, 0x01, 0xBE, 0xA0, 0xFF, 0x20, 0xC4, + 0xB8, 0xFF, 0x31, 0xCF, 0xD8, 0xFF, 0x31, 0xD5, 0xD1, 0xFF, 0x2E, 0xD5, 0xD4, 0xFF, 0x2A, 0xD4, 0xD7, 0xFF, 0x24, + 0xD7, 0xCC, 0xFF, 0x2E, 0xDE, 0xE8, 0xFF, 0x29, 0xDD, 0xE6, 0xFF, 0x24, 0xDC, 0xE4, 0xFF, 0x22, 0xD9, 0xED, 0xFF, + 0x20, 0xE1, 0xDF, 0xFF, 0x27, 0xE9, 0xEC, 0xFF, 0x1E, 0xE0, 0xEA, 0xFF, 0x1B, 0xD9, 0xE3, 0xFF, 0x19, 0xD3, 0xDD, + 0xFF, 0x1A, 0xD0, 0xE4, 0xFF, 0x1B, 0xCD, 0xEB, 0xFF, 0x17, 0xC7, 0xE4, 0xFF, 0x14, 0xC2, 0xDE, 0xFF, 0x00, 0xBC, + 0xEF, 0xFF, 0xEB, 0xA4, 0x1D, 0xFF, 0xFF, 0xA0, 0x19, 0xFF, 0xF6, 0xA2, 0x15, 0xFF, 0xF3, 0xA2, 0x19, 0xFF, 0xF0, + 0xA1, 0x1D, 0xFF, 0xF4, 0xA0, 0x19, 0xFF, 0xF8, 0x9F, 0x16, 0xFF, 0xF8, 0x9E, 0x15, 0xFF, 0xF8, 0x9D, 0x15, 0xFF, + 0xF7, 0x9B, 0x14, 0xFF, 0xF7, 0x9A, 0x14, 0xFF, 0xF6, 0x99, 0x13, 0xFF, 0xF5, 0x98, 0x12, 0xFF, 0xF5, 0x98, 0x12, + 0xFF, 0xF8, 0xD5, 0x5E, 0xFF, 0xFC, 0xD5, 0x63, 0xFF, 0xFF, 0xD6, 0x68, 0xFF, 0xFB, 0xD2, 0x5E, 0xFF, 0xF8, 0xCF, + 0x55, 0xFF, 0xF7, 0xCF, 0x53, 0xFF, 0xF7, 0xCE, 0x50, 0xFF, 0xF9, 0xCD, 0x4D, 0xFF, 0xFA, 0xCC, 0x4B, 0xFF, 0xFA, + 0xCB, 0x49, 0xFF, 0xFA, 0xCA, 0x48, 0xFF, 0xFA, 0xC9, 0x47, 0xFF, 0xFA, 0xC8, 0x45, 0xFF, 0xFA, 0xC7, 0x44, 0xFF, + 0xF9, 0xC6, 0x42, 0xFF, 0xF9, 0xC5, 0x41, 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x3F, 0xFF, 0xF9, 0xC2, 0x3D, + 0xFF, 0xF9, 0xC1, 0x3C, 0xFF, 0xF8, 0xC0, 0x3B, 0xFF, 0xF8, 0xC0, 0x3B, 0xFF, 0xF8, 0xC1, 0x3A, 0xFF, 0xF7, 0xBF, + 0x38, 0xFF, 0xF6, 0xBD, 0x35, 0xFF, 0xFA, 0xBD, 0x34, 0xFF, 0xFE, 0xBD, 0x33, 0xFF, 0xF5, 0xC3, 0x22, 0xFF, 0xFB, + 0xBA, 0x26, 0xFF, 0xB1, 0xB0, 0x53, 0xFF, 0x06, 0xC5, 0x9A, 0xFF, 0x22, 0xD2, 0xC0, 0xFF, 0x36, 0xDD, 0xD3, 0xFF, + 0x12, 0xBA, 0xB3, 0xFF, 0x1E, 0xC7, 0xC3, 0xFF, 0x21, 0xCE, 0xC4, 0xFF, 0x2C, 0xD8, 0xD8, 0xFF, 0x2F, 0xDA, 0xDE, + 0xFF, 0x2A, 0xD5, 0xDC, 0xFF, 0x20, 0xD4, 0xE7, 0xFF, 0x1C, 0xD5, 0xD4, 0xFF, 0x28, 0xE4, 0xE8, 0xFF, 0x24, 0xE3, + 0xEB, 0xFF, 0x1F, 0xCD, 0xD1, 0xFF, 0x1C, 0xC5, 0xD2, 0xFF, 0x01, 0xC2, 0xDC, 0xFF, 0x11, 0xC3, 0xCF, 0xFF, 0x09, + 0xC1, 0xE2, 0xFF, 0x00, 0xBE, 0xE3, 0xFF, 0x6E, 0xBE, 0x83, 0xFF, 0xF6, 0x9F, 0x0C, 0xFF, 0xFD, 0x9F, 0x11, 0xFF, + 0xF6, 0xA1, 0x17, 0xFF, 0xF4, 0xA1, 0x19, 0xFF, 0xF3, 0xA1, 0x1A, 0xFF, 0xF5, 0xA0, 0x18, 0xFF, 0xF8, 0x9F, 0x15, + 0xFF, 0xF7, 0x9E, 0x15, 0xFF, 0xF7, 0x9D, 0x14, 0xFF, 0xF7, 0x9C, 0x14, 0xFF, 0xF7, 0x9B, 0x13, 0xFF, 0xF5, 0x99, + 0x11, 0xFF, 0xF4, 0x98, 0x10, 0xFF, 0xF4, 0x98, 0x10, 0xFF, 0xFB, 0xD6, 0x64, 0xFF, 0xF9, 0xD4, 0x5D, 0xFF, 0xF8, + 0xD2, 0x55, 0xFF, 0xF8, 0xD0, 0x53, 0xFF, 0xF8, 0xCE, 0x50, 0xFF, 0xF9, 0xCE, 0x4E, 0xFF, 0xFA, 0xCD, 0x4D, 0xFF, + 0xFA, 0xCC, 0x4B, 0xFF, 0xFB, 0xCC, 0x49, 0xFF, 0xFA, 0xCB, 0x48, 0xFF, 0xFA, 0xCA, 0x47, 0xFF, 0xFA, 0xC9, 0x46, + 0xFF, 0xFA, 0xC8, 0x45, 0xFF, 0xFA, 0xC7, 0x43, 0xFF, 0xF9, 0xC6, 0x42, 0xFF, 0xF9, 0xC5, 0x41, 0xFF, 0xF9, 0xC4, + 0x40, 0xFF, 0xF9, 0xC3, 0x3F, 0xFF, 0xF9, 0xC2, 0x3D, 0xFF, 0xF9, 0xC1, 0x3C, 0xFF, 0xF8, 0xC0, 0x3B, 0xFF, 0xF8, + 0xC0, 0x3A, 0xFF, 0xF8, 0xBF, 0x39, 0xFF, 0xF8, 0xBF, 0x38, 0xFF, 0xF8, 0xBF, 0x36, 0xFF, 0xF7, 0xBD, 0x34, 0xFF, + 0xF7, 0xBC, 0x31, 0xFF, 0xF8, 0xBB, 0x33, 0xFF, 0xF9, 0xBA, 0x35, 0xFF, 0xFF, 0xBC, 0x2C, 0xFF, 0xDE, 0xC2, 0x60, + 0xFF, 0x84, 0xCB, 0x93, 0xFF, 0x2A, 0xD4, 0xC5, 0xFF, 0x2E, 0xD7, 0xCA, 0xFF, 0x12, 0xBA, 0xB0, 0xFF, 0x16, 0xBE, + 0xB4, 0xFF, 0x1A, 0xC2, 0xB8, 0xFF, 0x25, 0xC8, 0xC6, 0xFF, 0x20, 0xBE, 0xC4, 0xFF, 0x16, 0xC8, 0xDA, 0xFF, 0x18, + 0xC8, 0xC9, 0xFF, 0x21, 0xD7, 0xDB, 0xFF, 0x1A, 0xD6, 0xDD, 0xFF, 0x0D, 0xBC, 0xB7, 0xFF, 0x03, 0xBD, 0xC7, 0xFF, + 0x00, 0xBF, 0xD0, 0xFF, 0x50, 0xC9, 0xAC, 0xFF, 0xB0, 0xB8, 0x6B, 0xFF, 0xFF, 0xA3, 0x04, 0xFF, 0xFA, 0xA3, 0x12, + 0xFF, 0xF4, 0xA4, 0x21, 0xFF, 0xF5, 0xA2, 0x1D, 0xFF, 0xF5, 0xA1, 0x19, 0xFF, 0xF6, 0xA0, 0x18, 0xFF, 0xF6, 0xA0, + 0x17, 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, 0xFF, 0xF7, 0x9D, 0x14, 0xFF, 0xF6, + 0x9C, 0x13, 0xFF, 0xF6, 0x9B, 0x12, 0xFF, 0xF4, 0x99, 0x10, 0xFF, 0xF2, 0x97, 0x0E, 0xFF, 0xF2, 0x97, 0x0E, 0xFF, + 0xF8, 0xD4, 0x5C, 0xFF, 0xF8, 0xD3, 0x57, 0xFF, 0xF8, 0xD1, 0x53, 0xFF, 0xF8, 0xD0, 0x51, 0xFF, 0xF9, 0xCE, 0x4F, + 0xFF, 0xF9, 0xCE, 0x4D, 0xFF, 0xF9, 0xCD, 0x4B, 0xFF, 0xFA, 0xCC, 0x4A, 0xFF, 0xFA, 0xCB, 0x48, 0xFF, 0xFA, 0xCA, + 0x47, 0xFF, 0xFA, 0xCA, 0x46, 0xFF, 0xF9, 0xC9, 0x45, 0xFF, 0xF9, 0xC8, 0x44, 0xFF, 0xF9, 0xC7, 0x43, 0xFF, 0xF9, + 0xC6, 0x42, 0xFF, 0xF9, 0xC5, 0x41, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3D, 0xFF, + 0xF9, 0xC1, 0x3C, 0xFF, 0xF9, 0xC0, 0x3A, 0xFF, 0xF8, 0xBF, 0x39, 0xFF, 0xF8, 0xBF, 0x38, 0xFF, 0xF8, 0xBF, 0x37, + 0xFF, 0xF8, 0xBE, 0x36, 0xFF, 0xF5, 0xBD, 0x35, 0xFF, 0xF3, 0xBB, 0x34, 0xFF, 0xF7, 0xB9, 0x34, 0xFF, 0xFA, 0xB7, + 0x34, 0xFF, 0xFF, 0xB5, 0x22, 0xFF, 0xFE, 0xB4, 0x2E, 0xFF, 0xE6, 0xB9, 0x4D, 0xFF, 0xCD, 0xBF, 0x6B, 0xFF, 0xC5, + 0xB1, 0x27, 0xFF, 0x7C, 0xBB, 0x6C, 0xFF, 0x48, 0xBD, 0x89, 0xFF, 0x15, 0xBE, 0xA6, 0xFF, 0x08, 0xBF, 0xB9, 0xFF, + 0x00, 0xBF, 0xCB, 0xFF, 0x3D, 0xC4, 0xDA, 0xFF, 0x20, 0xCA, 0xBA, 0xFF, 0x3E, 0xC6, 0xAD, 0xFF, 0x53, 0xBB, 0x99, + 0xFF, 0x8A, 0xAC, 0x59, 0xFF, 0xC3, 0xAA, 0x35, 0xFF, 0xFF, 0xB3, 0x03, 0xFF, 0xFF, 0xA6, 0x15, 0xFF, 0xFF, 0xA4, + 0x20, 0xFF, 0xFA, 0xA0, 0x19, 0xFF, 0xF9, 0xA2, 0x1B, 0xFF, 0xF8, 0xA4, 0x1C, 0xFF, 0xF7, 0xA2, 0x1B, 0xFF, 0xF6, + 0xA1, 0x19, 0xFF, 0xF6, 0xA0, 0x18, 0xFF, 0xF7, 0xA0, 0x17, 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, + 0xF7, 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x14, 0xFF, 0xF6, 0x9C, 0x13, 0xFF, 0xF6, 0x9B, 0x12, 0xFF, 0xF4, 0x9A, 0x10, + 0xFF, 0xF3, 0x98, 0x0F, 0xFF, 0xF3, 0x98, 0x0F, 0xFF, 0xF6, 0xD2, 0x53, 0xFF, 0xF7, 0xD1, 0x52, 0xFF, 0xF8, 0xD0, + 0x51, 0xFF, 0xF8, 0xCF, 0x4F, 0xFF, 0xF9, 0xCE, 0x4E, 0xFF, 0xF9, 0xCE, 0x4C, 0xFF, 0xF9, 0xCD, 0x4A, 0xFF, 0xF9, + 0xCC, 0x48, 0xFF, 0xF9, 0xCB, 0x46, 0xFF, 0xF9, 0xCA, 0x46, 0xFF, 0xF9, 0xC9, 0x45, 0xFF, 0xF9, 0xC8, 0x44, 0xFF, + 0xF9, 0xC8, 0x43, 0xFF, 0xF9, 0xC7, 0x42, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, 0xC5, 0x41, 0xFF, 0xF9, 0xC4, 0x40, + 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3D, 0xFF, 0xF9, 0xC1, 0x3B, 0xFF, 0xF9, 0xC0, 0x3A, 0xFF, 0xF9, 0xBF, + 0x38, 0xFF, 0xF8, 0xBF, 0x37, 0xFF, 0xF8, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x35, 0xFF, 0xF4, 0xBC, 0x36, 0xFF, 0xEF, + 0xBA, 0x37, 0xFF, 0xF5, 0xB7, 0x35, 0xFF, 0xFC, 0xB5, 0x34, 0xFF, 0xF8, 0xB5, 0x2B, 0xFF, 0xF5, 0xB6, 0x22, 0xFF, + 0xFA, 0xB5, 0x25, 0xFF, 0xFF, 0xB3, 0x28, 0xFF, 0xFF, 0xB5, 0x28, 0xFF, 0xFF, 0xB7, 0x28, 0xFF, 0xFF, 0xB4, 0x1E, + 0xFF, 0xFE, 0xB2, 0x14, 0xFF, 0xF6, 0xAD, 0x20, 0xFF, 0xFE, 0xB9, 0x3C, 0xFF, 0xF0, 0xCB, 0x5A, 0xFF, 0xFA, 0xBE, + 0x41, 0xFF, 0xFC, 0xB5, 0x29, 0xFF, 0xFE, 0xAD, 0x11, 0xFF, 0xFC, 0xAC, 0x17, 0xFF, 0xFA, 0xAB, 0x1D, 0xFF, 0xFD, + 0xA9, 0x1D, 0xFF, 0xFF, 0xA7, 0x1D, 0xFF, 0xFA, 0xA7, 0x1B, 0xFF, 0xF4, 0xA8, 0x18, 0xFF, 0xF8, 0xA6, 0x18, 0xFF, + 0xFC, 0xA4, 0x17, 0xFF, 0xFA, 0xA2, 0x19, 0xFF, 0xF7, 0xA1, 0x1A, 0xFF, 0xF7, 0xA0, 0x19, 0xFF, 0xF7, 0xA0, 0x17, + 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, + 0x13, 0xFF, 0xF5, 0x9B, 0x12, 0xFF, 0xF4, 0x9A, 0x11, 0xFF, 0xF3, 0x99, 0x10, 0xFF, 0xF3, 0x99, 0x10, 0xFF, 0xF7, + 0xD1, 0x54, 0xFF, 0xF8, 0xD0, 0x52, 0xFF, 0xF8, 0xD0, 0x51, 0xFF, 0xF9, 0xCF, 0x4F, 0xFF, 0xF9, 0xCF, 0x4E, 0xFF, + 0xF9, 0xCE, 0x4B, 0xFF, 0xF9, 0xCD, 0x49, 0xFF, 0xF9, 0xCC, 0x47, 0xFF, 0xF8, 0xCA, 0x45, 0xFF, 0xF8, 0xCA, 0x44, + 0xFF, 0xF8, 0xC9, 0x44, 0xFF, 0xF9, 0xC8, 0x43, 0xFF, 0xF9, 0xC7, 0x42, 0xFF, 0xF9, 0xC7, 0x42, 0xFF, 0xF9, 0xC6, + 0x41, 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, + 0xC1, 0x3B, 0xFF, 0xF9, 0xC0, 0x39, 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF8, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x35, 0xFF, + 0xF8, 0xBD, 0x34, 0xFF, 0xF6, 0xBC, 0x34, 0xFF, 0xF4, 0xBA, 0x35, 0xFF, 0xF8, 0xB8, 0x34, 0xFF, 0xFB, 0xB6, 0x33, + 0xFF, 0xF9, 0xB6, 0x2D, 0xFF, 0xF6, 0xB6, 0x28, 0xFF, 0xF8, 0xB5, 0x29, 0xFF, 0xFA, 0xB4, 0x29, 0xFF, 0xFB, 0xB4, + 0x29, 0xFF, 0xFC, 0xB5, 0x29, 0xFF, 0xF5, 0xB2, 0x29, 0xFF, 0xEF, 0xAF, 0x29, 0xFF, 0xF5, 0xA9, 0x1A, 0xFF, 0xD9, + 0xCE, 0x9A, 0xFF, 0xE8, 0xCF, 0x6C, 0xFF, 0xE3, 0xC6, 0x73, 0xFF, 0xDD, 0xC9, 0x7F, 0xFF, 0xFB, 0xAD, 0x18, 0xFF, + 0xF9, 0xAC, 0x1B, 0xFF, 0xF7, 0xAB, 0x1F, 0xFF, 0xF9, 0xA9, 0x1E, 0xFF, 0xFB, 0xA7, 0x1D, 0xFF, 0xF8, 0xA7, 0x1C, + 0xFF, 0xF6, 0xA6, 0x1A, 0xFF, 0xF8, 0xA5, 0x19, 0xFF, 0xFA, 0xA3, 0x19, 0xFF, 0xF9, 0xA2, 0x1A, 0xFF, 0xF8, 0xA1, + 0x1A, 0xFF, 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, + 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, 0xFF, 0xF5, 0x9B, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, + 0xF4, 0x9A, 0x10, 0xFF, 0xF4, 0x9A, 0x10, 0xFF, 0xF9, 0xD0, 0x54, 0xFF, 0xF9, 0xCF, 0x52, 0xFF, 0xF9, 0xCF, 0x51, + 0xFF, 0xFA, 0xCF, 0x4F, 0xFF, 0xFA, 0xCF, 0x4D, 0xFF, 0xF9, 0xCE, 0x4A, 0xFF, 0xF9, 0xCC, 0x48, 0xFF, 0xF8, 0xCB, + 0x46, 0xFF, 0xF8, 0xCA, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC8, 0x42, 0xFF, 0xF8, + 0xC7, 0x42, 0xFF, 0xF8, 0xC7, 0x41, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, + 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, 0xC1, 0x3A, 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF9, 0xBF, 0x37, + 0xFF, 0xF9, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x34, 0xFF, 0xF8, 0xBD, 0x33, 0xFF, 0xF9, 0xBC, 0x33, 0xFF, 0xF9, 0xBA, + 0x32, 0xFF, 0xFA, 0xB9, 0x32, 0xFF, 0xFB, 0xB7, 0x31, 0xFF, 0xF9, 0xB6, 0x30, 0xFF, 0xF8, 0xB6, 0x2E, 0xFF, 0xF6, + 0xB5, 0x2C, 0xFF, 0xF4, 0xB4, 0x2A, 0xFF, 0xF5, 0xB3, 0x2A, 0xFF, 0xF6, 0xB2, 0x2A, 0xFF, 0xF9, 0xB2, 0x29, 0xFF, + 0xFB, 0xB2, 0x28, 0xFF, 0xF6, 0xB2, 0x30, 0xFF, 0xFD, 0xA8, 0x11, 0xFF, 0xE1, 0xD3, 0x7E, 0xFF, 0xE6, 0xBB, 0x58, + 0xFF, 0xFB, 0xAA, 0x15, 0xFF, 0xF7, 0xAD, 0x1F, 0xFF, 0xF6, 0xAB, 0x1F, 0xFF, 0xF5, 0xAA, 0x20, 0xFF, 0xF6, 0xA9, + 0x1F, 0xFF, 0xF6, 0xA7, 0x1E, 0xFF, 0xF7, 0xA6, 0x1D, 0xFF, 0xF8, 0xA5, 0x1B, 0xFF, 0xF8, 0xA4, 0x1B, 0xFF, 0xF8, + 0xA3, 0x1B, 0xFF, 0xF8, 0xA2, 0x1B, 0xFF, 0xF9, 0xA1, 0x1A, 0xFF, 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, + 0xF8, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, + 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF9, 0xD0, + 0x54, 0xFF, 0xF9, 0xCF, 0x52, 0xFF, 0xF9, 0xCF, 0x51, 0xFF, 0xFA, 0xCF, 0x4F, 0xFF, 0xFA, 0xCF, 0x4D, 0xFF, 0xF9, + 0xCE, 0x4A, 0xFF, 0xF9, 0xCC, 0x48, 0xFF, 0xF8, 0xCB, 0x46, 0xFF, 0xF8, 0xCA, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, + 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC8, 0x42, 0xFF, 0xF8, 0xC7, 0x42, 0xFF, 0xF8, 0xC7, 0x41, 0xFF, 0xF9, 0xC6, 0x41, + 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, 0xC1, + 0x3A, 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF9, 0xBF, 0x37, 0xFF, 0xF9, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x34, 0xFF, 0xF8, + 0xBD, 0x33, 0xFF, 0xF9, 0xBC, 0x33, 0xFF, 0xF9, 0xBA, 0x32, 0xFF, 0xFA, 0xB9, 0x32, 0xFF, 0xFB, 0xB7, 0x31, 0xFF, + 0xF9, 0xB6, 0x30, 0xFF, 0xF8, 0xB6, 0x2E, 0xFF, 0xF6, 0xB5, 0x2C, 0xFF, 0xF4, 0xB4, 0x2A, 0xFF, 0xF5, 0xB3, 0x2A, + 0xFF, 0xF6, 0xB2, 0x2A, 0xFF, 0xF8, 0xB2, 0x2A, 0xFF, 0xFA, 0xB1, 0x29, 0xFF, 0xF4, 0xB5, 0x2D, 0xFF, 0xF5, 0xB4, + 0x1D, 0xFF, 0xFF, 0x9B, 0x23, 0xFF, 0xF2, 0xB5, 0x1F, 0xFF, 0xFB, 0xAB, 0x0B, 0xFF, 0xF6, 0xAC, 0x1E, 0xFF, 0xF6, + 0xAB, 0x1F, 0xFF, 0xF5, 0xAA, 0x20, 0xFF, 0xF6, 0xA9, 0x1F, 0xFF, 0xF6, 0xA7, 0x1E, 0xFF, 0xF7, 0xA6, 0x1D, 0xFF, + 0xF8, 0xA5, 0x1B, 0xFF, 0xF8, 0xA4, 0x1B, 0xFF, 0xF8, 0xA3, 0x1B, 0xFF, 0xF8, 0xA2, 0x1B, 0xFF, 0xF9, 0xA1, 0x1A, + 0xFF, 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, 0xF8, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, + 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, + 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF9, 0xD0, 0x54, 0xFF, 0xF9, 0xCF, 0x52, 0xFF, 0xF9, 0xCF, 0x51, 0xFF, + 0xFA, 0xCF, 0x4F, 0xFF, 0xFA, 0xCF, 0x4D, 0xFF, 0xF9, 0xCE, 0x4A, 0xFF, 0xF9, 0xCC, 0x48, 0xFF, 0xF8, 0xCB, 0x46, + 0xFF, 0xF8, 0xCA, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC8, 0x42, 0xFF, 0xF8, 0xC7, + 0x42, 0xFF, 0xF8, 0xC7, 0x41, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, + 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, 0xC1, 0x3A, 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF9, 0xBF, 0x37, 0xFF, + 0xF9, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x34, 0xFF, 0xF8, 0xBD, 0x33, 0xFF, 0xF9, 0xBC, 0x33, 0xFF, 0xF9, 0xBA, 0x32, + 0xFF, 0xFA, 0xB9, 0x32, 0xFF, 0xFB, 0xB7, 0x31, 0xFF, 0xF9, 0xB6, 0x30, 0xFF, 0xF8, 0xB6, 0x2E, 0xFF, 0xF6, 0xB5, + 0x2C, 0xFF, 0xF4, 0xB4, 0x2A, 0xFF, 0xF5, 0xB3, 0x2A, 0xFF, 0xF6, 0xB2, 0x2A, 0xFF, 0xF7, 0xB2, 0x2A, 0xFF, 0xF8, + 0xB1, 0x2A, 0xFF, 0xF9, 0xAE, 0x21, 0xFF, 0xFA, 0xAC, 0x18, 0xFF, 0xF6, 0xAD, 0x1E, 0xFF, 0xF3, 0xAE, 0x23, 0xFF, + 0xF4, 0xAC, 0x20, 0xFF, 0xF5, 0xAB, 0x1D, 0xFF, 0xF5, 0xAA, 0x1E, 0xFF, 0xF5, 0xAA, 0x20, 0xFF, 0xF6, 0xA9, 0x1F, + 0xFF, 0xF6, 0xA7, 0x1E, 0xFF, 0xF7, 0xA6, 0x1D, 0xFF, 0xF8, 0xA5, 0x1B, 0xFF, 0xF8, 0xA4, 0x1B, 0xFF, 0xF8, 0xA3, + 0x1B, 0xFF, 0xF8, 0xA2, 0x1B, 0xFF, 0xF9, 0xA1, 0x1A, 0xFF, 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, 0xF8, + 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, 0xFF, + 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF9, 0xD0, 0x54, + 0xFF, 0xF9, 0xCF, 0x52, 0xFF, 0xF9, 0xCF, 0x51, 0xFF, 0xFA, 0xCF, 0x4F, 0xFF, 0xFA, 0xCF, 0x4D, 0xFF, 0xF9, 0xCE, + 0x4A, 0xFF, 0xF9, 0xCC, 0x48, 0xFF, 0xF8, 0xCB, 0x46, 0xFF, 0xF8, 0xCA, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, + 0xC9, 0x43, 0xFF, 0xF8, 0xC8, 0x42, 0xFF, 0xF8, 0xC7, 0x42, 0xFF, 0xF8, 0xC7, 0x41, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, + 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, 0xC1, 0x3A, + 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF9, 0xBF, 0x37, 0xFF, 0xF9, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x34, 0xFF, 0xF8, 0xBD, + 0x33, 0xFF, 0xF9, 0xBC, 0x33, 0xFF, 0xF9, 0xBA, 0x32, 0xFF, 0xFA, 0xB9, 0x32, 0xFF, 0xFB, 0xB7, 0x31, 0xFF, 0xF9, + 0xB6, 0x30, 0xFF, 0xF8, 0xB6, 0x2E, 0xFF, 0xF6, 0xB5, 0x2C, 0xFF, 0xF4, 0xB4, 0x2A, 0xFF, 0xF5, 0xB3, 0x2A, 0xFF, + 0xF6, 0xB2, 0x2A, 0xFF, 0xF7, 0xB2, 0x2A, 0xFF, 0xF8, 0xB1, 0x2A, 0xFF, 0xF9, 0xAE, 0x21, 0xFF, 0xFA, 0xAC, 0x18, + 0xFF, 0xF6, 0xAD, 0x1E, 0xFF, 0xF3, 0xAE, 0x23, 0xFF, 0xF4, 0xAC, 0x20, 0xFF, 0xF5, 0xAB, 0x1D, 0xFF, 0xF5, 0xAA, + 0x1E, 0xFF, 0xF5, 0xAA, 0x20, 0xFF, 0xF6, 0xA9, 0x1F, 0xFF, 0xF6, 0xA7, 0x1E, 0xFF, 0xF7, 0xA6, 0x1D, 0xFF, 0xF8, + 0xA5, 0x1B, 0xFF, 0xF8, 0xA4, 0x1B, 0xFF, 0xF8, 0xA3, 0x1B, 0xFF, 0xF8, 0xA2, 0x1B, 0xFF, 0xF9, 0xA1, 0x1A, 0xFF, + 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, 0xF8, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, + 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, + 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, +]; + +const CONVERTED_TO_XRGB_BUFFER: [u8; 64 * 64 * 4] = [ + 0xFF, 0x22, 0x9B, 0xDE, 0xFF, 0x23, 0x9D, 0xE0, 0xFF, 0x25, 0x9E, 0xE1, 0xFF, 0x2B, 0xA5, 0xE8, 0xFF, 0x22, 0x9B, + 0xDF, 0xFF, 0x22, 0x9C, 0xDF, 0xFF, 0x22, 0x9C, 0xE0, 0xFF, 0x22, 0x9C, 0xDF, 0xFF, 0x21, 0x9B, 0xDF, 0xFF, 0x22, + 0x9B, 0xDF, 0xFF, 0x23, 0x9B, 0xDF, 0xFF, 0x23, 0x9B, 0xDF, 0xFF, 0x24, 0x9C, 0xDF, 0xFF, 0x21, 0x9B, 0xE2, 0xFF, + 0x1D, 0x9B, 0xE5, 0xFF, 0x1F, 0x9A, 0xE1, 0xFF, 0x21, 0x98, 0xDD, 0xFF, 0x21, 0x99, 0xDE, 0xFF, 0x20, 0x99, 0xDE, + 0xFF, 0x1F, 0x9A, 0xDF, 0xFF, 0x1F, 0x9A, 0xE0, 0xFF, 0x1E, 0x99, 0xE0, 0xFF, 0x1D, 0x99, 0xDF, 0xFF, 0x1C, 0x98, + 0xDF, 0xFF, 0x1B, 0x97, 0xDF, 0xFF, 0x1E, 0x95, 0xDC, 0xFF, 0x21, 0x93, 0xD8, 0xFF, 0x1F, 0x93, 0xDC, 0xFF, 0x1C, + 0x93, 0xE0, 0xFF, 0x1A, 0x94, 0xDC, 0xFF, 0x18, 0x95, 0xD8, 0xFF, 0x1C, 0x91, 0xDB, 0xFF, 0x1F, 0x8E, 0xDE, 0xFF, + 0x1A, 0x90, 0xDE, 0xFF, 0x16, 0x93, 0xDE, 0xFF, 0x17, 0x92, 0xDF, 0xFF, 0x18, 0x91, 0xDF, 0xFF, 0x17, 0x90, 0xDF, + 0xFF, 0x17, 0x8F, 0xDE, 0xFF, 0x16, 0x8E, 0xDE, 0xFF, 0x15, 0x8C, 0xDE, 0xFF, 0x14, 0x8C, 0xDD, 0xFF, 0x13, 0x8C, + 0xDB, 0xFF, 0x12, 0x8C, 0xDA, 0xFF, 0x11, 0x8C, 0xD9, 0xFF, 0x11, 0x8B, 0xD9, 0xFF, 0x11, 0x89, 0xD9, 0xFF, 0x11, + 0x88, 0xDA, 0xFF, 0x12, 0x87, 0xDA, 0xFF, 0x11, 0x86, 0xDA, 0xFF, 0x10, 0x86, 0xDA, 0xFF, 0x10, 0x85, 0xD9, 0xFF, + 0x0F, 0x84, 0xD9, 0xFF, 0x0E, 0x83, 0xD9, 0xFF, 0x0E, 0x83, 0xD8, 0xFF, 0x0D, 0x82, 0xD8, 0xFF, 0x0C, 0x81, 0xD8, + 0xFF, 0x0C, 0x80, 0xD7, 0xFF, 0x0D, 0x7F, 0xD7, 0xFF, 0x0D, 0x7F, 0xD6, 0xFF, 0x0D, 0x7E, 0xD6, 0xFF, 0x0D, 0x7E, + 0xD6, 0xFF, 0x0D, 0x7E, 0xD6, 0xFF, 0x0D, 0x7E, 0xD6, 0xFF, 0x24, 0x9F, 0xE0, 0xFF, 0x27, 0xA0, 0xE1, 0xFF, 0x29, + 0xA2, 0xE2, 0xFF, 0x2A, 0xA4, 0xE5, 0xFF, 0x24, 0x9E, 0xE0, 0xFF, 0x24, 0x9E, 0xE1, 0xFF, 0x24, 0x9E, 0xE1, 0xFF, + 0x23, 0x9E, 0xE1, 0xFF, 0x23, 0x9D, 0xE1, 0xFF, 0x23, 0x9D, 0xE1, 0xFF, 0x24, 0x9D, 0xE1, 0xFF, 0x24, 0x9D, 0xE1, + 0xFF, 0x25, 0x9D, 0xE1, 0xFF, 0x23, 0x9D, 0xE1, 0xFF, 0x22, 0x9C, 0xE2, 0xFF, 0x22, 0x9C, 0xE0, 0xFF, 0x22, 0x9B, + 0xDF, 0xFF, 0x21, 0x9B, 0xE0, 0xFF, 0x20, 0x9B, 0xE1, 0xFF, 0x20, 0x9B, 0xE1, 0xFF, 0x1F, 0x9B, 0xE1, 0xFF, 0x20, + 0x9A, 0xDF, 0xFF, 0x20, 0x99, 0xDE, 0xFF, 0x1E, 0x98, 0xDE, 0xFF, 0x1D, 0x97, 0xDF, 0xFF, 0x1D, 0x97, 0xDF, 0xFF, + 0x1E, 0x96, 0xDF, 0xFF, 0x1D, 0x95, 0xDF, 0xFF, 0x1C, 0x94, 0xDE, 0xFF, 0x1C, 0x94, 0xDF, 0xFF, 0x1B, 0x93, 0xE0, + 0xFF, 0x1C, 0x93, 0xE0, 0xFF, 0x1D, 0x92, 0xE0, 0xFF, 0x1B, 0x93, 0xDE, 0xFF, 0x19, 0x94, 0xDC, 0xFF, 0x19, 0x93, + 0xDE, 0xFF, 0x19, 0x92, 0xE0, 0xFF, 0x19, 0x91, 0xDF, 0xFF, 0x18, 0x90, 0xDF, 0xFF, 0x17, 0x8F, 0xDF, 0xFF, 0x17, + 0x8E, 0xDF, 0xFF, 0x16, 0x8E, 0xDE, 0xFF, 0x15, 0x8D, 0xDD, 0xFF, 0x13, 0x8D, 0xDC, 0xFF, 0x12, 0x8D, 0xDB, 0xFF, + 0x12, 0x8C, 0xDB, 0xFF, 0x12, 0x8B, 0xDB, 0xFF, 0x12, 0x89, 0xDB, 0xFF, 0x12, 0x88, 0xDB, 0xFF, 0x11, 0x87, 0xDB, + 0xFF, 0x11, 0x87, 0xDB, 0xFF, 0x10, 0x86, 0xDB, 0xFF, 0x0F, 0x85, 0xDB, 0xFF, 0x0E, 0x84, 0xDA, 0xFF, 0x0D, 0x83, + 0xD9, 0xFF, 0x0D, 0x83, 0xD9, 0xFF, 0x0D, 0x83, 0xD9, 0xFF, 0x0D, 0x82, 0xD8, 0xFF, 0x0D, 0x81, 0xD8, 0xFF, 0x0D, + 0x80, 0xD7, 0xFF, 0x0D, 0x7F, 0xD7, 0xFF, 0x0D, 0x7F, 0xD7, 0xFF, 0x0D, 0x7F, 0xD7, 0xFF, 0x0D, 0x7F, 0xD7, 0xFF, + 0x27, 0xA2, 0xE2, 0xFF, 0x2A, 0xA4, 0xE3, 0xFF, 0x2D, 0xA5, 0xE3, 0xFF, 0x29, 0xA3, 0xE3, 0xFF, 0x26, 0xA1, 0xE2, + 0xFF, 0x25, 0xA1, 0xE2, 0xFF, 0x25, 0xA1, 0xE2, 0xFF, 0x25, 0xA0, 0xE2, 0xFF, 0x24, 0xA0, 0xE2, 0xFF, 0x25, 0x9F, + 0xE2, 0xFF, 0x25, 0x9F, 0xE3, 0xFF, 0x25, 0x9E, 0xE3, 0xFF, 0x26, 0x9E, 0xE3, 0xFF, 0x26, 0x9E, 0xE1, 0xFF, 0x27, + 0x9D, 0xDE, 0xFF, 0x24, 0x9D, 0xDF, 0xFF, 0x22, 0x9E, 0xE1, 0xFF, 0x21, 0x9D, 0xE2, 0xFF, 0x20, 0x9D, 0xE3, 0xFF, + 0x20, 0x9D, 0xE3, 0xFF, 0x20, 0x9C, 0xE3, 0xFF, 0x22, 0x9B, 0xDF, 0xFF, 0x24, 0x99, 0xDC, 0xFF, 0x21, 0x98, 0xDE, + 0xFF, 0x1F, 0x98, 0xE0, 0xFF, 0x1D, 0x99, 0xE3, 0xFF, 0x1B, 0x9A, 0xE7, 0xFF, 0x1B, 0x98, 0xE1, 0xFF, 0x1C, 0x96, + 0xDC, 0xFF, 0x1D, 0x94, 0xE2, 0xFF, 0x1F, 0x92, 0xE9, 0xFF, 0x1D, 0x94, 0xE5, 0xFF, 0x1A, 0x96, 0xE2, 0xFF, 0x1B, + 0x95, 0xDE, 0xFF, 0x1D, 0x95, 0xDA, 0xFF, 0x1C, 0x94, 0xDD, 0xFF, 0x1A, 0x93, 0xE0, 0xFF, 0x1A, 0x92, 0xE0, 0xFF, + 0x19, 0x91, 0xE0, 0xFF, 0x19, 0x91, 0xDF, 0xFF, 0x18, 0x90, 0xDF, 0xFF, 0x17, 0x8F, 0xDE, 0xFF, 0x16, 0x8F, 0xDE, + 0xFF, 0x15, 0x8E, 0xDD, 0xFF, 0x14, 0x8E, 0xDD, 0xFF, 0x14, 0x8D, 0xDC, 0xFF, 0x13, 0x8C, 0xDC, 0xFF, 0x12, 0x8B, + 0xDC, 0xFF, 0x12, 0x8A, 0xDB, 0xFF, 0x11, 0x89, 0xDC, 0xFF, 0x11, 0x88, 0xDC, 0xFF, 0x10, 0x87, 0xDC, 0xFF, 0x10, + 0x86, 0xDC, 0xFF, 0x0E, 0x84, 0xDB, 0xFF, 0x0D, 0x83, 0xD9, 0xFF, 0x0E, 0x83, 0xD9, 0xFF, 0x0E, 0x84, 0xDA, 0xFF, + 0x0E, 0x83, 0xD9, 0xFF, 0x0E, 0x82, 0xD9, 0xFF, 0x0D, 0x80, 0xD8, 0xFF, 0x0D, 0x7F, 0xD8, 0xFF, 0x0D, 0x7F, 0xD8, + 0xFF, 0x0D, 0x7F, 0xD8, 0xFF, 0x0D, 0x7F, 0xD8, 0xFF, 0x29, 0xA6, 0xE4, 0xFF, 0x2D, 0xA7, 0xE3, 0xFF, 0x30, 0xA8, + 0xE3, 0xFF, 0x2C, 0xA6, 0xE3, 0xFF, 0x27, 0xA3, 0xE3, 0xFF, 0x27, 0xA3, 0xE3, 0xFF, 0x26, 0xA3, 0xE3, 0xFF, 0x26, + 0xA2, 0xE4, 0xFF, 0x26, 0xA2, 0xE4, 0xFF, 0x26, 0xA1, 0xE4, 0xFF, 0x26, 0xA1, 0xE4, 0xFF, 0x26, 0xA0, 0xE5, 0xFF, + 0x26, 0x9F, 0xE5, 0xFF, 0x25, 0xA0, 0xE4, 0xFF, 0x24, 0xA0, 0xE4, 0xFF, 0x24, 0x9F, 0xE3, 0xFF, 0x24, 0x9E, 0xE3, + 0xFF, 0x23, 0x9E, 0xE4, 0xFF, 0x21, 0x9F, 0xE6, 0xFF, 0x21, 0x9F, 0xE5, 0xFF, 0x22, 0x9E, 0xE3, 0xFF, 0x13, 0xA4, + 0xE5, 0xFF, 0x1A, 0x9F, 0xE7, 0xFF, 0x15, 0x9F, 0xE7, 0xFF, 0x10, 0xA0, 0xE7, 0xFF, 0x11, 0x9F, 0xEF, 0xFF, 0x12, + 0x9E, 0xF7, 0xFF, 0x1A, 0x99, 0xEC, 0xFF, 0x17, 0x9A, 0xE1, 0xFF, 0x14, 0x9C, 0xE3, 0xFF, 0x1C, 0x98, 0xE5, 0xFF, + 0x1C, 0x97, 0xE6, 0xFF, 0x1B, 0x96, 0xE6, 0xFF, 0x1B, 0x98, 0xDB, 0xFF, 0x1C, 0x96, 0xDF, 0xFF, 0x1C, 0x95, 0xE0, + 0xFF, 0x1B, 0x94, 0xE1, 0xFF, 0x1B, 0x93, 0xE1, 0xFF, 0x1A, 0x93, 0xE0, 0xFF, 0x1A, 0x92, 0xE0, 0xFF, 0x19, 0x92, + 0xE0, 0xFF, 0x18, 0x91, 0xDF, 0xFF, 0x18, 0x90, 0xDF, 0xFF, 0x17, 0x8F, 0xDF, 0xFF, 0x16, 0x8F, 0xDF, 0xFF, 0x15, + 0x8E, 0xDE, 0xFF, 0x14, 0x8D, 0xDD, 0xFF, 0x13, 0x8C, 0xDD, 0xFF, 0x12, 0x8B, 0xDC, 0xFF, 0x12, 0x8A, 0xDC, 0xFF, + 0x11, 0x89, 0xDD, 0xFF, 0x11, 0x87, 0xDD, 0xFF, 0x10, 0x86, 0xDE, 0xFF, 0x0F, 0x85, 0xDC, 0xFF, 0x0D, 0x83, 0xD9, + 0xFF, 0x0E, 0x84, 0xDA, 0xFF, 0x0F, 0x85, 0xDB, 0xFF, 0x0F, 0x84, 0xDA, 0xFF, 0x0E, 0x83, 0xDA, 0xFF, 0x0E, 0x81, + 0xDA, 0xFF, 0x0D, 0x80, 0xD9, 0xFF, 0x0D, 0x80, 0xD9, 0xFF, 0x0D, 0x80, 0xD9, 0xFF, 0x0D, 0x80, 0xD9, 0xFF, 0x2C, + 0xAA, 0xE7, 0xFF, 0x30, 0xAA, 0xE4, 0xFF, 0x33, 0xAA, 0xE2, 0xFF, 0x2E, 0xA8, 0xE3, 0xFF, 0x28, 0xA5, 0xE4, 0xFF, + 0x28, 0xA5, 0xE5, 0xFF, 0x28, 0xA5, 0xE5, 0xFF, 0x28, 0xA4, 0xE5, 0xFF, 0x27, 0xA4, 0xE5, 0xFF, 0x27, 0xA3, 0xE6, + 0xFF, 0x27, 0xA2, 0xE6, 0xFF, 0x27, 0xA1, 0xE7, 0xFF, 0x27, 0xA1, 0xE7, 0xFF, 0x25, 0xA2, 0xE8, 0xFF, 0x22, 0xA3, + 0xE9, 0xFF, 0x24, 0xA0, 0xE7, 0xFF, 0x27, 0x9E, 0xE6, 0xFF, 0x25, 0x9F, 0xE7, 0xFF, 0x22, 0xA0, 0xE8, 0xFF, 0x18, + 0xA3, 0xF4, 0xFF, 0x0D, 0xA7, 0xFF, 0xFF, 0x1A, 0xA5, 0xDD, 0xFF, 0x54, 0x8D, 0xBA, 0xFF, 0x6E, 0x83, 0x9C, 0xFF, + 0x88, 0x79, 0x7D, 0xFF, 0x8C, 0x79, 0x7B, 0xFF, 0x91, 0x79, 0x79, 0xFF, 0x7E, 0x7A, 0x94, 0xFF, 0x55, 0x87, 0xAF, + 0xFF, 0x21, 0x9B, 0xD6, 0xFF, 0x04, 0xA3, 0xFD, 0xFF, 0x0F, 0x9D, 0xF4, 0xFF, 0x1B, 0x96, 0xEB, 0xFF, 0x1B, 0x9A, + 0xD9, 0xFF, 0x1B, 0x98, 0xE4, 0xFF, 0x1C, 0x96, 0xE3, 0xFF, 0x1C, 0x95, 0xE2, 0xFF, 0x1C, 0x94, 0xE2, 0xFF, 0x1B, + 0x94, 0xE1, 0xFF, 0x1B, 0x94, 0xE1, 0xFF, 0x1B, 0x93, 0xE0, 0xFF, 0x1A, 0x92, 0xE0, 0xFF, 0x19, 0x91, 0xE0, 0xFF, + 0x18, 0x90, 0xE1, 0xFF, 0x18, 0x8F, 0xE1, 0xFF, 0x16, 0x8F, 0xE0, 0xFF, 0x15, 0x8E, 0xDF, 0xFF, 0x14, 0x8D, 0xDE, + 0xFF, 0x12, 0x8C, 0xDC, 0xFF, 0x12, 0x8B, 0xDD, 0xFF, 0x12, 0x8A, 0xDE, 0xFF, 0x11, 0x88, 0xDF, 0xFF, 0x11, 0x87, + 0xE0, 0xFF, 0x0F, 0x85, 0xDD, 0xFF, 0x0D, 0x83, 0xDA, 0xFF, 0x0E, 0x85, 0xDB, 0xFF, 0x10, 0x87, 0xDC, 0xFF, 0x0F, + 0x85, 0xDC, 0xFF, 0x0F, 0x84, 0xDB, 0xFF, 0x0E, 0x82, 0xDB, 0xFF, 0x0D, 0x81, 0xDA, 0xFF, 0x0D, 0x81, 0xDA, 0xFF, + 0x0D, 0x81, 0xDA, 0xFF, 0x0D, 0x81, 0xDA, 0xFF, 0x30, 0xAA, 0xE4, 0xFF, 0x35, 0xAF, 0xE8, 0xFF, 0x33, 0xAB, 0xE3, + 0xFF, 0x2F, 0xA9, 0xE5, 0xFF, 0x2A, 0xA8, 0xE6, 0xFF, 0x35, 0xAD, 0xE8, 0xFF, 0x25, 0xA6, 0xE7, 0xFF, 0x28, 0xA7, + 0xE7, 0xFF, 0x2B, 0xA8, 0xE7, 0xFF, 0x2D, 0xA6, 0xE5, 0xFF, 0x2E, 0xA4, 0xE4, 0xFF, 0x2B, 0xA4, 0xE6, 0xFF, 0x29, + 0xA4, 0xE8, 0xFF, 0x2A, 0xA4, 0xE5, 0xFF, 0x2C, 0xA5, 0xE1, 0xFF, 0x10, 0xA9, 0xEF, 0xFF, 0x12, 0xAD, 0xF6, 0xFF, + 0x22, 0xA2, 0xF8, 0xFF, 0x60, 0x91, 0xA5, 0xFF, 0xA5, 0x75, 0x5C, 0xFF, 0xEB, 0x59, 0x14, 0xFF, 0xFF, 0x48, 0x0C, + 0xFF, 0xFA, 0x55, 0x03, 0xFF, 0xFF, 0x59, 0x0F, 0xFF, 0xFF, 0x5D, 0x1A, 0xFF, 0xFF, 0x60, 0x16, 0xFF, 0xF9, 0x64, + 0x11, 0xFF, 0xFF, 0x54, 0x0F, 0xFF, 0xFF, 0x4A, 0x0C, 0xFF, 0xFA, 0x49, 0x17, 0xFF, 0xF5, 0x47, 0x23, 0xFF, 0x8D, + 0x72, 0x7E, 0xFF, 0x26, 0x9D, 0xD9, 0xFF, 0x05, 0xA1, 0xFF, 0xFF, 0x1D, 0x96, 0xE1, 0xFF, 0x17, 0x98, 0xE9, 0xFF, + 0x1C, 0x97, 0xE3, 0xFF, 0x1A, 0x97, 0xE3, 0xFF, 0x18, 0x97, 0xE4, 0xFF, 0x19, 0x96, 0xE3, 0xFF, 0x1B, 0x94, 0xE2, + 0xFF, 0x1A, 0x93, 0xE1, 0xFF, 0x19, 0x93, 0xE0, 0xFF, 0x18, 0x92, 0xE1, 0xFF, 0x17, 0x91, 0xE1, 0xFF, 0x16, 0x90, + 0xE0, 0xFF, 0x15, 0x8F, 0xDF, 0xFF, 0x14, 0x8E, 0xDE, 0xFF, 0x13, 0x8D, 0xDD, 0xFF, 0x13, 0x8D, 0xDE, 0xFF, 0x13, + 0x8C, 0xDF, 0xFF, 0x12, 0x8A, 0xDF, 0xFF, 0x10, 0x89, 0xE0, 0xFF, 0x0F, 0x87, 0xDD, 0xFF, 0x0E, 0x84, 0xDB, 0xFF, + 0x13, 0x8A, 0xDF, 0xFF, 0x0F, 0x87, 0xDB, 0xFF, 0x0F, 0x86, 0xDC, 0xFF, 0x0F, 0x85, 0xDC, 0xFF, 0x0E, 0x84, 0xDB, + 0xFF, 0x0D, 0x82, 0xDB, 0xFF, 0x0D, 0x82, 0xDB, 0xFF, 0x0D, 0x82, 0xDB, 0xFF, 0x0D, 0x82, 0xDB, 0xFF, 0x33, 0xAB, + 0xE2, 0xFF, 0x3B, 0xB3, 0xEB, 0xFF, 0x33, 0xAC, 0xE5, 0xFF, 0x30, 0xAB, 0xE6, 0xFF, 0x2D, 0xAA, 0xE7, 0xFF, 0x43, + 0xB6, 0xEA, 0xFF, 0x23, 0xA7, 0xEA, 0xFF, 0x29, 0xA9, 0xE9, 0xFF, 0x2F, 0xAB, 0xE9, 0xFF, 0x32, 0xA9, 0xE5, 0xFF, + 0x35, 0xA7, 0xE2, 0xFF, 0x30, 0xA7, 0xE6, 0xFF, 0x2A, 0xA8, 0xEA, 0xFF, 0x25, 0xAA, 0xF0, 0xFF, 0x1F, 0xAD, 0xF6, + 0xFF, 0x4D, 0x8A, 0xA7, 0xFF, 0xB7, 0x66, 0x4C, 0xFF, 0xFF, 0x54, 0x0F, 0xFF, 0xF7, 0x64, 0x0C, 0xFF, 0xF8, 0x63, + 0x13, 0xFF, 0xF9, 0x61, 0x1A, 0xFF, 0xEF, 0x67, 0x1E, 0xFF, 0xFC, 0x61, 0x22, 0xFF, 0xFA, 0x68, 0x25, 0xFF, 0xF9, + 0x6F, 0x28, 0xFF, 0xF5, 0x70, 0x22, 0xFF, 0xF2, 0x72, 0x1B, 0xFF, 0xF2, 0x6B, 0x1F, 0xFF, 0xF1, 0x64, 0x24, 0xFF, + 0xFF, 0x55, 0x21, 0xFF, 0xFF, 0x53, 0x1E, 0xFF, 0xFF, 0x4B, 0x16, 0xFF, 0xFF, 0x43, 0x0E, 0xFF, 0xB1, 0x61, 0x5A, + 0xFF, 0x1E, 0x95, 0xDF, 0xFF, 0x12, 0x9A, 0xF0, 0xFF, 0x1B, 0x9A, 0xE5, 0xFF, 0x18, 0x9A, 0xE5, 0xFF, 0x14, 0x9A, + 0xE6, 0xFF, 0x17, 0x98, 0xE5, 0xFF, 0x1B, 0x95, 0xE4, 0xFF, 0x1A, 0x95, 0xE2, 0xFF, 0x19, 0x94, 0xE0, 0xFF, 0x18, + 0x93, 0xE1, 0xFF, 0x17, 0x92, 0xE2, 0xFF, 0x16, 0x91, 0xE1, 0xFF, 0x16, 0x90, 0xE0, 0xFF, 0x15, 0x8F, 0xDF, 0xFF, + 0x14, 0x8F, 0xDE, 0xFF, 0x14, 0x8E, 0xDF, 0xFF, 0x14, 0x8E, 0xE1, 0xFF, 0x12, 0x8C, 0xE0, 0xFF, 0x10, 0x8A, 0xE0, + 0xFF, 0x10, 0x88, 0xDE, 0xFF, 0x10, 0x86, 0xDC, 0xFF, 0x17, 0x8E, 0xE3, 0xFF, 0x0D, 0x87, 0xDB, 0xFF, 0x0E, 0x86, + 0xDB, 0xFF, 0x0F, 0x86, 0xDC, 0xFF, 0x0E, 0x85, 0xDC, 0xFF, 0x0E, 0x83, 0xDB, 0xFF, 0x0E, 0x83, 0xDB, 0xFF, 0x0E, + 0x83, 0xDB, 0xFF, 0x0E, 0x83, 0xDB, 0xFF, 0x36, 0xB0, 0xEA, 0xFF, 0x36, 0xB3, 0xEF, 0xFF, 0x2E, 0xAE, 0xED, 0xFF, + 0x2C, 0xAD, 0xEC, 0xFF, 0x2A, 0xAD, 0xEB, 0xFF, 0x40, 0xB3, 0xEF, 0xFF, 0x28, 0xAA, 0xE9, 0xFF, 0x2B, 0xAB, 0xE7, + 0xFF, 0x2F, 0xAB, 0xE6, 0xFF, 0x30, 0xAA, 0xE6, 0xFF, 0x31, 0xAA, 0xE5, 0xFF, 0x2E, 0xA9, 0xE6, 0xFF, 0x2B, 0xA9, + 0xE7, 0xFF, 0x24, 0xA7, 0xEB, 0xFF, 0x93, 0x6A, 0x5F, 0xFF, 0xFF, 0x3D, 0x05, 0xFF, 0xF9, 0x56, 0x17, 0xFF, 0xE2, + 0x72, 0x12, 0xFF, 0xF8, 0x72, 0x29, 0xFF, 0xF7, 0x74, 0x27, 0xFF, 0xF6, 0x76, 0x25, 0xFF, 0xF1, 0x76, 0x28, 0xFF, + 0xF8, 0x70, 0x2A, 0xFF, 0xF8, 0x77, 0x2D, 0xFF, 0xF9, 0x7D, 0x30, 0xFF, 0xF7, 0x7F, 0x2D, 0xFF, 0xF5, 0x81, 0x2A, + 0xFF, 0xF5, 0x7B, 0x2B, 0xFF, 0xF5, 0x75, 0x2C, 0xFF, 0xFD, 0x6A, 0x2B, 0xFF, 0xFA, 0x64, 0x2A, 0xFF, 0xF5, 0x5D, + 0x2C, 0xFF, 0xF0, 0x57, 0x2E, 0xFF, 0xFF, 0x48, 0x10, 0xFF, 0xFF, 0x45, 0x0E, 0xFF, 0x80, 0x76, 0x7F, 0xFF, 0x02, + 0xA7, 0xF0, 0xFF, 0x24, 0x95, 0xEA, 0xFF, 0x19, 0x9A, 0xE3, 0xFF, 0x1B, 0x98, 0xE4, 0xFF, 0x1D, 0x95, 0xE4, 0xFF, + 0x1B, 0x95, 0xE2, 0xFF, 0x19, 0x96, 0xDF, 0xFF, 0x18, 0x94, 0xE1, 0xFF, 0x17, 0x93, 0xE2, 0xFF, 0x16, 0x92, 0xE2, + 0xFF, 0x16, 0x92, 0xE1, 0xFF, 0x15, 0x91, 0xE0, 0xFF, 0x15, 0x90, 0xDF, 0xFF, 0x15, 0x90, 0xE0, 0xFF, 0x15, 0x91, + 0xE2, 0xFF, 0x12, 0x8E, 0xE1, 0xFF, 0x0F, 0x8C, 0xDF, 0xFF, 0x12, 0x8B, 0xDF, 0xFF, 0x14, 0x8A, 0xDF, 0xFF, 0x15, + 0x8D, 0xE2, 0xFF, 0x0E, 0x89, 0xDC, 0xFF, 0x0E, 0x88, 0xDC, 0xFF, 0x0F, 0x87, 0xDD, 0xFF, 0x0E, 0x86, 0xDC, 0xFF, + 0x0E, 0x85, 0xDC, 0xFF, 0x0E, 0x85, 0xDC, 0xFF, 0x0E, 0x85, 0xDC, 0xFF, 0x0E, 0x85, 0xDC, 0xFF, 0x5F, 0xC0, 0xE6, + 0xFF, 0x57, 0xBE, 0xE8, 0xFF, 0x4F, 0xBB, 0xE9, 0xFF, 0x4E, 0xBA, 0xE6, 0xFF, 0x4D, 0xB9, 0xE3, 0xFF, 0x50, 0xB6, + 0xED, 0xFF, 0x2D, 0xAE, 0xE7, 0xFF, 0x2E, 0xAC, 0xE6, 0xFF, 0x2E, 0xAB, 0xE4, 0xFF, 0x2E, 0xAC, 0xE6, 0xFF, 0x2E, + 0xAD, 0xE8, 0xFF, 0x2D, 0xAB, 0xE7, 0xFF, 0x2C, 0xAA, 0xE5, 0xFF, 0x15, 0xB2, 0xFF, 0xFF, 0xEB, 0x42, 0x10, 0xFF, + 0xF1, 0x4F, 0x16, 0xFF, 0xF7, 0x5C, 0x1C, 0xFF, 0xF8, 0x71, 0x23, 0xFF, 0xF9, 0x85, 0x29, 0xFF, 0xF6, 0x88, 0x2D, + 0xFF, 0xF3, 0x8B, 0x30, 0xFF, 0xF4, 0x85, 0x31, 0xFF, 0xF4, 0x7F, 0x33, 0xFF, 0xF6, 0x85, 0x35, 0xFF, 0xF9, 0x8B, + 0x37, 0xFF, 0xF8, 0x8D, 0x38, 0xFF, 0xF7, 0x90, 0x3A, 0xFF, 0xF8, 0x8B, 0x37, 0xFF, 0xF8, 0x86, 0x35, 0xFF, 0xF7, + 0x7E, 0x35, 0xFF, 0xF6, 0x75, 0x35, 0xFF, 0xF7, 0x6D, 0x33, 0xFF, 0xF7, 0x64, 0x31, 0xFF, 0xF8, 0x5E, 0x31, 0xFF, + 0xF8, 0x57, 0x30, 0xFF, 0xFF, 0x51, 0x25, 0xFF, 0xF5, 0x51, 0x36, 0xFF, 0x03, 0xA4, 0xFD, 0xFF, 0x1E, 0x9A, 0xE1, + 0xFF, 0x1E, 0x98, 0xE3, 0xFF, 0x1E, 0x96, 0xE5, 0xFF, 0x1C, 0x96, 0xE2, 0xFF, 0x19, 0x97, 0xDF, 0xFF, 0x18, 0x96, + 0xE1, 0xFF, 0x17, 0x95, 0xE3, 0xFF, 0x16, 0x94, 0xE2, 0xFF, 0x16, 0x93, 0xE1, 0xFF, 0x16, 0x92, 0xE0, 0xFF, 0x15, + 0x91, 0xE0, 0xFF, 0x16, 0x92, 0xE2, 0xFF, 0x16, 0x93, 0xE4, 0xFF, 0x12, 0x90, 0xE1, 0xFF, 0x0F, 0x8E, 0xDF, 0xFF, + 0x14, 0x8D, 0xE1, 0xFF, 0x18, 0x8D, 0xE3, 0xFF, 0x13, 0x8C, 0xE0, 0xFF, 0x0F, 0x8B, 0xDE, 0xFF, 0x0F, 0x89, 0xDD, + 0xFF, 0x0E, 0x88, 0xDD, 0xFF, 0x0E, 0x87, 0xDD, 0xFF, 0x0E, 0x86, 0xDC, 0xFF, 0x0E, 0x86, 0xDC, 0xFF, 0x0E, 0x86, + 0xDC, 0xFF, 0x0E, 0x86, 0xDC, 0xFF, 0x3C, 0xB6, 0xED, 0xFF, 0x35, 0xB3, 0xEE, 0xFF, 0x2F, 0xB1, 0xEF, 0xFF, 0x2F, + 0xB1, 0xED, 0xFF, 0x2F, 0xB0, 0xEC, 0xFF, 0x38, 0xB0, 0xEE, 0xFF, 0x2D, 0xAE, 0xE9, 0xFF, 0x2F, 0xAD, 0xE7, 0xFF, + 0x30, 0xAD, 0xE6, 0xFF, 0x2F, 0xAE, 0xE8, 0xFF, 0x2D, 0xB0, 0xEA, 0xFF, 0x30, 0xAD, 0xEC, 0xFF, 0x28, 0xAF, 0xEE, + 0xFF, 0x2F, 0xA9, 0xC8, 0xFF, 0xFF, 0x3D, 0x04, 0xFF, 0xFA, 0x50, 0x19, 0xFF, 0xF8, 0x5F, 0x21, 0xFF, 0xF7, 0x73, + 0x28, 0xFF, 0xF7, 0x87, 0x2F, 0xFF, 0xFA, 0x95, 0x37, 0xFF, 0xF5, 0x9B, 0x37, 0xFF, 0xF5, 0x96, 0x3A, 0xFF, 0xF5, + 0x92, 0x3D, 0xFF, 0xF7, 0x94, 0x3F, 0xFF, 0xF9, 0x96, 0x41, 0xFF, 0xF9, 0x99, 0x43, 0xFF, 0xF9, 0x9D, 0x46, 0xFF, + 0xF8, 0x98, 0x44, 0xFF, 0xF7, 0x94, 0x43, 0xFF, 0xF8, 0x8D, 0x42, 0xFF, 0xF9, 0x86, 0x41, 0xFF, 0xF9, 0x7D, 0x3F, + 0xFF, 0xF9, 0x73, 0x3C, 0xFF, 0xF7, 0x70, 0x38, 0xFF, 0xF4, 0x6C, 0x35, 0xFF, 0xFF, 0x60, 0x21, 0xFF, 0xBE, 0x6C, + 0x62, 0xFF, 0x12, 0x9D, 0xEF, 0xFF, 0x21, 0x9A, 0xE8, 0xFF, 0x1C, 0x99, 0xED, 0xFF, 0x17, 0x9B, 0xE3, 0xFF, 0x13, + 0x98, 0xF0, 0xFF, 0x1B, 0x94, 0xE0, 0xFF, 0x1A, 0x96, 0xE1, 0xFF, 0x19, 0x97, 0xE3, 0xFF, 0x18, 0x96, 0xE4, 0xFF, + 0x17, 0x95, 0xE5, 0xFF, 0x18, 0x94, 0xE3, 0xFF, 0x19, 0x93, 0xE2, 0xFF, 0x16, 0x91, 0xE0, 0xFF, 0x14, 0x90, 0xDE, + 0xFF, 0x15, 0x91, 0xE1, 0xFF, 0x16, 0x92, 0xE5, 0xFF, 0x14, 0x90, 0xE3, 0xFF, 0x11, 0x8D, 0xE2, 0xFF, 0x10, 0x8D, + 0xE2, 0xFF, 0x0F, 0x8D, 0xE3, 0xFF, 0x10, 0x8A, 0xDE, 0xFF, 0x11, 0x88, 0xD8, 0xFF, 0x0E, 0x87, 0xE1, 0xFF, 0x0B, + 0x89, 0xDC, 0xFF, 0x10, 0x85, 0xE0, 0xFF, 0x09, 0x87, 0xE4, 0xFF, 0x09, 0x87, 0xE4, 0xFF, 0x3F, 0xB5, 0xE8, 0xFF, + 0x3B, 0xB3, 0xE9, 0xFF, 0x36, 0xB2, 0xEA, 0xFF, 0x37, 0xB1, 0xE9, 0xFF, 0x37, 0xB1, 0xE8, 0xFF, 0x32, 0xAF, 0xE9, + 0xFF, 0x2D, 0xAE, 0xEA, 0xFF, 0x30, 0xAE, 0xE9, 0xFF, 0x32, 0xAF, 0xE8, 0xFF, 0x30, 0xB1, 0xEA, 0xFF, 0x2D, 0xB4, + 0xEC, 0xFF, 0x34, 0xAE, 0xF1, 0xFF, 0x24, 0xB4, 0xF6, 0xFF, 0x8D, 0x7E, 0x86, 0xFF, 0xF6, 0x4E, 0x00, 0xFF, 0xEC, + 0x5C, 0x1D, 0xFF, 0xF9, 0x63, 0x25, 0xFF, 0xF7, 0x76, 0x2D, 0xFF, 0xF4, 0x89, 0x35, 0xFF, 0xFD, 0xA2, 0x41, 0xFF, + 0xF6, 0xAB, 0x3E, 0xFF, 0xF6, 0xA8, 0x43, 0xFF, 0xF7, 0xA4, 0x47, 0xFF, 0xF8, 0xA3, 0x4A, 0xFF, 0xFA, 0xA1, 0x4C, + 0xFF, 0xFA, 0xA5, 0x4E, 0xFF, 0xFB, 0xAA, 0x51, 0xFF, 0xF9, 0xA6, 0x52, 0xFF, 0xF7, 0xA2, 0x52, 0xFF, 0xFA, 0x9C, + 0x4F, 0xFF, 0xFD, 0x97, 0x4D, 0xFF, 0xFC, 0x8D, 0x4A, 0xFF, 0xFB, 0x83, 0x47, 0xFF, 0xF6, 0x82, 0x40, 0xFF, 0xF1, + 0x82, 0x39, 0xFF, 0xF4, 0x72, 0x2B, 0xFF, 0x71, 0x8C, 0xAB, 0xFF, 0x16, 0x99, 0xF0, 0xFF, 0x25, 0x99, 0xEF, 0xFF, + 0x25, 0x97, 0xE8, 0xFF, 0x26, 0x9A, 0xC5, 0xFF, 0x16, 0x96, 0xF0, 0xFF, 0x1C, 0x91, 0xE2, 0xFF, 0x1B, 0x96, 0xE2, + 0xFF, 0x1B, 0x9A, 0xE2, 0xFF, 0x19, 0x99, 0xE5, 0xFF, 0x18, 0x98, 0xE8, 0xFF, 0x1A, 0x96, 0xE6, 0xFF, 0x1C, 0x95, + 0xE4, 0xFF, 0x17, 0x91, 0xDF, 0xFF, 0x13, 0x8D, 0xD9, 0xFF, 0x18, 0x92, 0xE2, 0xFF, 0x1E, 0x97, 0xEA, 0xFF, 0x14, + 0x92, 0xE5, 0xFF, 0x0B, 0x8D, 0xE1, 0xFF, 0x0D, 0x8E, 0xE5, 0xFF, 0x10, 0x8F, 0xE9, 0xFF, 0x12, 0x8B, 0xDE, 0xFF, + 0x14, 0x88, 0xD4, 0xFF, 0x0E, 0x87, 0xE6, 0xFF, 0x08, 0x8C, 0xDC, 0xFF, 0x11, 0x84, 0xE4, 0xFF, 0x03, 0x88, 0xEC, + 0xFF, 0x03, 0x88, 0xEC, 0xFF, 0x3D, 0xB6, 0xEA, 0xFF, 0x3A, 0xB5, 0xEA, 0xFF, 0x38, 0xB4, 0xEB, 0xFF, 0x37, 0xB3, + 0xEB, 0xFF, 0x37, 0xB3, 0xEA, 0xFF, 0x34, 0xB2, 0xEB, 0xFF, 0x32, 0xB1, 0xEB, 0xFF, 0x33, 0xB1, 0xEB, 0xFF, 0x34, + 0xB0, 0xEA, 0xFF, 0x32, 0xB3, 0xE9, 0xFF, 0x2F, 0xB5, 0xE8, 0xFF, 0x34, 0xB0, 0xF0, 0xFF, 0x22, 0xB6, 0xF8, 0xFF, + 0xC5, 0x60, 0x44, 0xFF, 0xF9, 0x53, 0x0B, 0xFF, 0xF2, 0x63, 0x21, 0xFF, 0xF6, 0x6F, 0x29, 0xFF, 0xF6, 0x7D, 0x2F, + 0xFF, 0xF7, 0x8A, 0x35, 0xFF, 0xFA, 0xA1, 0x41, 0xFF, 0xF6, 0xAF, 0x45, 0xFF, 0xFA, 0xB4, 0x4F, 0xFF, 0xF6, 0xB0, + 0x50, 0xFF, 0xF8, 0xAE, 0x53, 0xFF, 0xFA, 0xAC, 0x56, 0xFF, 0xFC, 0xB2, 0x59, 0xFF, 0xFD, 0xB7, 0x5D, 0xFF, 0xFA, + 0xB3, 0x5F, 0xFF, 0xF6, 0xAF, 0x61, 0xFF, 0xF9, 0xAC, 0x5D, 0xFF, 0xFD, 0xA9, 0x59, 0xFF, 0xFB, 0x9F, 0x55, 0xFF, + 0xF8, 0x94, 0x50, 0xFF, 0xF7, 0x91, 0x4A, 0xFF, 0xF5, 0x8D, 0x44, 0xFF, 0xFF, 0x7D, 0x22, 0xFF, 0x1A, 0xA5, 0xEF, + 0xFF, 0x12, 0x9E, 0xF3, 0xFF, 0x28, 0x96, 0xF1, 0xFF, 0x22, 0x9F, 0xB0, 0xFF, 0x6C, 0x96, 0x00, 0xFF, 0x3B, 0x9B, + 0x82, 0xFF, 0x16, 0x9D, 0xF8, 0xFF, 0x15, 0x9B, 0xF4, 0xFF, 0x14, 0x9C, 0xE2, 0xFF, 0x15, 0x99, 0xE4, 0xFF, 0x17, + 0x96, 0xE6, 0xFF, 0x18, 0x95, 0xE5, 0xFF, 0x1A, 0x93, 0xE4, 0xFF, 0x18, 0x93, 0xE2, 0xFF, 0x16, 0x92, 0xE0, 0xFF, + 0x1C, 0x98, 0xE6, 0xFF, 0x19, 0x95, 0xE4, 0xFF, 0x16, 0x92, 0xE4, 0xFF, 0x12, 0x8F, 0xE5, 0xFF, 0x12, 0x8C, 0xEB, + 0xFF, 0x12, 0x8B, 0xE3, 0xFF, 0x00, 0x87, 0xE3, 0xFF, 0x00, 0x7B, 0xF4, 0xFF, 0x1A, 0x86, 0xD3, 0xFF, 0x0C, 0x8C, + 0xF0, 0xFF, 0x00, 0x8E, 0xE2, 0xFF, 0x0D, 0x84, 0xEA, 0xFF, 0x07, 0x86, 0xF1, 0xFF, 0x3B, 0xB7, 0xEC, 0xFF, 0x3A, + 0xB6, 0xEC, 0xFF, 0x39, 0xB6, 0xEC, 0xFF, 0x38, 0xB5, 0xEC, 0xFF, 0x37, 0xB5, 0xED, 0xFF, 0x37, 0xB4, 0xEC, 0xFF, + 0x37, 0xB4, 0xEC, 0xFF, 0x36, 0xB3, 0xEC, 0xFF, 0x36, 0xB2, 0xEC, 0xFF, 0x33, 0xB4, 0xE8, 0xFF, 0x31, 0xB5, 0xE4, + 0xFF, 0x34, 0xB1, 0xEF, 0xFF, 0x21, 0xB8, 0xF9, 0xFF, 0xFD, 0x41, 0x02, 0xFF, 0xFC, 0x58, 0x1E, 0xFF, 0xF8, 0x6A, + 0x25, 0xFF, 0xF3, 0x7C, 0x2C, 0xFF, 0xF6, 0x84, 0x31, 0xFF, 0xF9, 0x8B, 0x35, 0xFF, 0xF7, 0xA0, 0x41, 0xFF, 0xF6, + 0xB4, 0x4C, 0xFF, 0xFE, 0xC0, 0x5B, 0xFF, 0xF6, 0xBC, 0x59, 0xFF, 0xF8, 0xBA, 0x5D, 0xFF, 0xFA, 0xB7, 0x60, 0xFF, + 0xFD, 0xBE, 0x64, 0xFF, 0xFF, 0xC4, 0x69, 0xFF, 0xFA, 0xC0, 0x6C, 0xFF, 0xF5, 0xBD, 0x6F, 0xFF, 0xF9, 0xBC, 0x6A, + 0xFF, 0xFD, 0xBB, 0x65, 0xFF, 0xFA, 0xB1, 0x60, 0xFF, 0xF6, 0xA6, 0x5A, 0xFF, 0xF8, 0x9F, 0x54, 0xFF, 0xFA, 0x98, + 0x4F, 0xFF, 0xDF, 0x94, 0x6E, 0xFF, 0x07, 0xA6, 0xFB, 0xFF, 0x24, 0x9C, 0xDA, 0xFF, 0x14, 0x9F, 0xF2, 0xFF, 0x4A, + 0xA1, 0x71, 0xFF, 0x68, 0xA9, 0x0D, 0xFF, 0x61, 0xA3, 0x06, 0xFF, 0x5A, 0x98, 0x1B, 0xFF, 0x33, 0x96, 0x9B, 0xFF, + 0x0D, 0x99, 0xFE, 0xFF, 0x11, 0x96, 0xF1, 0xFF, 0x16, 0x94, 0xE4, 0xFF, 0x17, 0x93, 0xE4, 0xFF, 0x18, 0x91, 0xE4, + 0xFF, 0x19, 0x94, 0xE5, 0xFF, 0x1A, 0x98, 0xE6, 0xFF, 0x1F, 0x9D, 0xEA, 0xFF, 0x15, 0x93, 0xDE, 0xFF, 0x17, 0x92, + 0xE3, 0xFF, 0x1A, 0x91, 0xE8, 0xFF, 0x1F, 0x94, 0xEB, 0xFF, 0x25, 0x9D, 0xD1, 0xFF, 0xD0, 0xF7, 0x72, 0xFF, 0xC1, + 0xF2, 0x95, 0xFF, 0x00, 0x83, 0xF0, 0xFF, 0x17, 0x81, 0xA0, 0xFF, 0x3B, 0x7E, 0x2E, 0xFF, 0x16, 0x87, 0xCB, 0xFF, + 0x0B, 0x8A, 0xDA, 0xFF, 0x3D, 0xB8, 0xEC, 0xFF, 0x3C, 0xB8, 0xED, 0xFF, 0x3B, 0xB7, 0xED, 0xFF, 0x3A, 0xB7, 0xED, + 0xFF, 0x39, 0xB6, 0xED, 0xFF, 0x39, 0xB6, 0xED, 0xFF, 0x39, 0xB6, 0xED, 0xFF, 0x39, 0xB6, 0xED, 0xFF, 0x39, 0xB6, + 0xED, 0xFF, 0x37, 0xB4, 0xEC, 0xFF, 0x34, 0xB2, 0xEB, 0xFF, 0x34, 0xAB, 0xF2, 0xFF, 0x6D, 0x95, 0xB3, 0xFF, 0xFF, + 0x46, 0x00, 0xFF, 0xF7, 0x64, 0x20, 0xFF, 0xF6, 0x73, 0x28, 0xFF, 0xF5, 0x81, 0x30, 0xFF, 0xF6, 0x8B, 0x37, 0xFF, + 0xF8, 0x94, 0x3D, 0xFF, 0xF8, 0xA6, 0x48, 0xFF, 0xF7, 0xB7, 0x53, 0xFF, 0xFB, 0xC2, 0x60, 0xFF, 0xF7, 0xC4, 0x65, + 0xFF, 0xF9, 0xC3, 0x69, 0xFF, 0xFA, 0xC2, 0x6D, 0xFF, 0xFA, 0xC6, 0x72, 0xFF, 0xFA, 0xCB, 0x77, 0xFF, 0xFB, 0xCB, + 0x7A, 0xFF, 0xFC, 0xCB, 0x7D, 0xFF, 0xFA, 0xC8, 0x7A, 0xFF, 0xF8, 0xC5, 0x77, 0xFF, 0xF9, 0xBC, 0x72, 0xFF, 0xFA, + 0xB4, 0x6C, 0xFF, 0xF6, 0xB0, 0x68, 0xFF, 0xFD, 0xAA, 0x56, 0xFF, 0x93, 0xA0, 0xA5, 0xFF, 0x13, 0xA1, 0xF3, 0xFF, + 0x21, 0x9C, 0xEF, 0xFF, 0x19, 0x9D, 0xFF, 0xFF, 0x71, 0xC1, 0x23, 0xFF, 0x79, 0xB7, 0x25, 0xFF, 0x71, 0xB2, 0x1D, + 0xFF, 0x6A, 0xAA, 0x23, 0xFF, 0x66, 0xA0, 0x25, 0xFF, 0x63, 0x9A, 0x18, 0xFF, 0x41, 0x9C, 0x72, 0xFF, 0x1E, 0x9F, + 0xCB, 0xFF, 0x18, 0x93, 0xFF, 0xFF, 0x13, 0x98, 0xF1, 0xFF, 0x18, 0x9C, 0xF4, 0xFF, 0x1D, 0xA0, 0xF7, 0xFF, 0x1B, + 0x9C, 0xFF, 0xFF, 0x10, 0x93, 0xF6, 0xFF, 0x11, 0x93, 0xF1, 0xFF, 0x13, 0x93, 0xEC, 0xFF, 0x00, 0x83, 0xFF, 0xFF, + 0x72, 0xCB, 0xA0, 0xFF, 0xCB, 0xF9, 0x81, 0xFF, 0xD0, 0xFF, 0xAC, 0xFF, 0x78, 0xA0, 0x45, 0xFF, 0x33, 0x77, 0x00, + 0xFF, 0x3A, 0x7C, 0x02, 0xFF, 0x0D, 0x8C, 0xE2, 0xFF, 0x0D, 0x8E, 0xDB, 0xFF, 0x3E, 0xBA, 0xED, 0xFF, 0x3D, 0xB9, + 0xED, 0xFF, 0x3C, 0xB9, 0xED, 0xFF, 0x3B, 0xB8, 0xED, 0xFF, 0x3A, 0xB8, 0xED, 0xFF, 0x3B, 0xB8, 0xED, 0xFF, 0x3B, + 0xB8, 0xED, 0xFF, 0x3C, 0xB8, 0xEE, 0xFF, 0x3C, 0xB9, 0xEE, 0xFF, 0x3A, 0xB4, 0xF0, 0xFF, 0x37, 0xAE, 0xF2, 0xFF, + 0x32, 0xB3, 0xFE, 0xFF, 0xB3, 0x8E, 0x7C, 0xFF, 0xFF, 0x58, 0x06, 0xFF, 0xF3, 0x71, 0x22, 0xFF, 0xF4, 0x7C, 0x2B, + 0xFF, 0xF6, 0x86, 0x34, 0xFF, 0xF7, 0x92, 0x3D, 0xFF, 0xF8, 0x9D, 0x45, 0xFF, 0xF8, 0xAC, 0x4F, 0xFF, 0xF8, 0xBB, + 0x5A, 0xFF, 0xF9, 0xC4, 0x65, 0xFF, 0xF9, 0xCC, 0x70, 0xFF, 0xFA, 0xCC, 0x75, 0xFF, 0xFA, 0xCC, 0x7A, 0xFF, 0xF7, + 0xCF, 0x80, 0xFF, 0xF4, 0xD2, 0x85, 0xFF, 0xFB, 0xD5, 0x89, 0xFF, 0xFF, 0xD9, 0x8C, 0xFF, 0xFA, 0xD3, 0x8B, 0xFF, + 0xF2, 0xCE, 0x89, 0xFF, 0xF8, 0xC8, 0x84, 0xFF, 0xFE, 0xC1, 0x7F, 0xFF, 0xF4, 0xC1, 0x7C, 0xFF, 0xFF, 0xBC, 0x5E, + 0xFF, 0x47, 0xAB, 0xDB, 0xFF, 0x1E, 0x9C, 0xEA, 0xFF, 0x1D, 0xA2, 0xE8, 0xFF, 0x1D, 0xA7, 0xE5, 0xFF, 0x98, 0xD3, + 0x1B, 0xFF, 0x8A, 0xCB, 0x21, 0xFF, 0x82, 0xC3, 0x26, 0xFF, 0x7A, 0xBB, 0x2C, 0xFF, 0x75, 0xB4, 0x28, 0xFF, 0x70, + 0xAD, 0x25, 0xFF, 0x6D, 0xAB, 0x16, 0xFF, 0x6A, 0xA9, 0x08, 0xFF, 0x5E, 0xA9, 0x11, 0xFF, 0x51, 0x9E, 0x53, 0xFF, + 0x47, 0x9B, 0x6D, 0xFF, 0x3E, 0x97, 0x87, 0xFF, 0x3B, 0x95, 0x91, 0xFF, 0x38, 0x98, 0x80, 0xFF, 0x44, 0x96, 0x63, + 0xFF, 0x4F, 0x94, 0x45, 0xFF, 0x82, 0xB4, 0x3C, 0xFF, 0x4F, 0x84, 0x1B, 0xFF, 0xAF, 0xE0, 0x87, 0xFF, 0x9E, 0xCC, + 0x82, 0xFF, 0x35, 0x7F, 0x11, 0xFF, 0x42, 0x82, 0x1B, 0xFF, 0x32, 0x84, 0x3B, 0xFF, 0x04, 0x92, 0xF9, 0xFF, 0x0F, + 0x92, 0xDC, 0xFF, 0x40, 0xBC, 0xEE, 0xFF, 0x3F, 0xBB, 0xED, 0xFF, 0x3E, 0xBA, 0xED, 0xFF, 0x3D, 0xBA, 0xED, 0xFF, + 0x3C, 0xB9, 0xEC, 0xFF, 0x3C, 0xB9, 0xEC, 0xFF, 0x3C, 0xB8, 0xEC, 0xFF, 0x3C, 0xB8, 0xEC, 0xFF, 0x3C, 0xB8, 0xEB, + 0xFF, 0x3F, 0xB3, 0xF0, 0xFF, 0x42, 0xAF, 0xF4, 0xFF, 0x0D, 0xBA, 0xE8, 0xFF, 0xFF, 0xB8, 0x96, 0xFF, 0xF6, 0x81, + 0x4C, 0xFF, 0xF5, 0x75, 0x22, 0xFF, 0xF6, 0x80, 0x2D, 0xFF, 0xF7, 0x8B, 0x38, 0xFF, 0xF7, 0x99, 0x42, 0xFF, 0xF7, + 0xA6, 0x4D, 0xFF, 0xF8, 0xB2, 0x56, 0xFF, 0xF9, 0xBD, 0x5F, 0xFF, 0xF9, 0xC8, 0x6D, 0xFF, 0xFA, 0xD4, 0x7A, 0xFF, + 0xFA, 0xD5, 0x81, 0xFF, 0xF9, 0xD7, 0x88, 0xFF, 0xFA, 0xD8, 0x8D, 0xFF, 0xFB, 0xDA, 0x92, 0xFF, 0xF9, 0xE4, 0xA1, + 0xFF, 0xFE, 0xD6, 0x91, 0xFF, 0xFA, 0xDE, 0x9F, 0xFF, 0xF8, 0xDB, 0x97, 0xFF, 0xF9, 0xD5, 0x93, 0xFF, 0xFB, 0xCF, + 0x8F, 0xFF, 0xFF, 0xD1, 0x85, 0xFF, 0xFF, 0xC6, 0x78, 0xFF, 0x00, 0x9A, 0xFC, 0xFF, 0x26, 0xA8, 0xF1, 0xFF, 0x1F, + 0xA4, 0xF8, 0xFF, 0x53, 0xBD, 0xA5, 0xFF, 0xA4, 0xDA, 0x30, 0xFF, 0x9D, 0xD5, 0x37, 0xFF, 0x97, 0xD0, 0x3A, 0xFF, + 0x90, 0xCA, 0x3D, 0xFF, 0x8A, 0xC5, 0x39, 0xFF, 0x84, 0xBF, 0x35, 0xFF, 0x7C, 0xBD, 0x30, 0xFF, 0x74, 0xBC, 0x2C, + 0xFF, 0x75, 0xB8, 0x1B, 0xFF, 0x77, 0xAF, 0x27, 0xFF, 0x72, 0xAB, 0x25, 0xFF, 0x6D, 0xA7, 0x23, 0xFF, 0x6A, 0xA3, + 0x28, 0xFF, 0x68, 0xA2, 0x1E, 0xFF, 0x57, 0x95, 0x19, 0xFF, 0x77, 0xB7, 0x45, 0xFF, 0xBA, 0xF0, 0x81, 0xFF, 0x72, + 0xAC, 0x4C, 0xFF, 0x41, 0x7B, 0x14, 0xFF, 0x4F, 0x8A, 0x1D, 0xFF, 0x42, 0x86, 0x1C, 0xFF, 0x49, 0x86, 0x14, 0xFF, + 0x16, 0x86, 0x8B, 0xFF, 0x0A, 0x90, 0xF5, 0xFF, 0x15, 0x8D, 0xE7, 0xFF, 0x41, 0xBE, 0xEF, 0xFF, 0x40, 0xBD, 0xEE, + 0xFF, 0x3F, 0xBC, 0xED, 0xFF, 0x3E, 0xBB, 0xED, 0xFF, 0x3D, 0xBA, 0xEC, 0xFF, 0x3D, 0xBA, 0xEB, 0xFF, 0x3C, 0xB9, + 0xEA, 0xFF, 0x3C, 0xB8, 0xE9, 0xFF, 0x3B, 0xB7, 0xE8, 0xFF, 0x39, 0xB9, 0xF0, 0xFF, 0x37, 0xBA, 0xF7, 0xFF, 0x50, + 0xB5, 0xDC, 0xFF, 0xFF, 0x96, 0x44, 0xFF, 0xFE, 0xC4, 0x9C, 0xFF, 0xF7, 0x79, 0x23, 0xFF, 0xF8, 0x85, 0x30, 0xFF, + 0xF8, 0x91, 0x3C, 0xFF, 0xF8, 0xA0, 0x48, 0xFF, 0xF7, 0xAF, 0x55, 0xFF, 0xF8, 0xB7, 0x5D, 0xFF, 0xF9, 0xBF, 0x65, + 0xFF, 0xFA, 0xCD, 0x75, 0xFF, 0xFB, 0xDB, 0x85, 0xFF, 0xFA, 0xDE, 0x8D, 0xFF, 0xF9, 0xE1, 0x95, 0xFF, 0xFD, 0xE1, + 0x9A, 0xFF, 0xFF, 0xE2, 0xA0, 0xFF, 0xFA, 0xE8, 0xA3, 0xFF, 0xFF, 0xBD, 0x6B, 0xFF, 0xFC, 0xDE, 0x9E, 0xFF, 0xFF, + 0xE8, 0xA6, 0xFF, 0xFB, 0xE3, 0xA3, 0xFF, 0xF7, 0xDE, 0xA0, 0xFF, 0xFD, 0xD7, 0x99, 0xFF, 0xB5, 0xBD, 0xAB, 0xFF, + 0x11, 0x9F, 0xF0, 0xFF, 0x1D, 0xA3, 0xE8, 0xFF, 0x19, 0x9E, 0xFF, 0xFF, 0x89, 0xD4, 0x65, 0xFF, 0xB0, 0xE1, 0x45, + 0xFF, 0xB0, 0xDF, 0x4D, 0xFF, 0xAB, 0xDC, 0x4D, 0xFF, 0xA7, 0xD8, 0x4D, 0xFF, 0xA0, 0xD5, 0x49, 0xFF, 0x99, 0xD2, + 0x44, 0xFF, 0x97, 0xCD, 0x3C, 0xFF, 0x94, 0xC9, 0x34, 0xFF, 0x8D, 0xC4, 0x34, 0xFF, 0x86, 0xC0, 0x33, 0xFF, 0x7A, + 0xBC, 0x32, 0xFF, 0x6E, 0xB7, 0x31, 0xFF, 0x6D, 0xB2, 0x2F, 0xFF, 0x6B, 0xAE, 0x2E, 0xFF, 0x7D, 0xB9, 0x3F, 0xFF, + 0x6F, 0xA5, 0x30, 0xFF, 0x7B, 0xB5, 0x4E, 0xFF, 0x56, 0x9A, 0x20, 0xFF, 0x5B, 0x9F, 0x2A, 0xFF, 0x50, 0x93, 0x24, + 0xFF, 0x80, 0xB9, 0x65, 0xFF, 0x5F, 0x99, 0x1C, 0xFF, 0x03, 0x8F, 0xE2, 0xFF, 0x10, 0x8E, 0xF2, 0xFF, 0x1B, 0x88, + 0xF2, 0xFF, 0x43, 0xBF, 0xEF, 0xFF, 0x42, 0xBE, 0xEE, 0xFF, 0x41, 0xBD, 0xEE, 0xFF, 0x40, 0xBD, 0xEE, 0xFF, 0x3F, + 0xBC, 0xED, 0xFF, 0x3F, 0xBB, 0xEC, 0xFF, 0x3F, 0xB9, 0xEB, 0xFF, 0x3D, 0xB9, 0xEC, 0xFF, 0x3C, 0xB8, 0xEE, 0xFF, + 0x37, 0xB8, 0xEB, 0xFF, 0x26, 0xBC, 0xF6, 0xFF, 0x94, 0x9B, 0x8F, 0xFF, 0xFB, 0x96, 0x37, 0xFF, 0xF9, 0xBB, 0x7C, + 0xFF, 0xF8, 0xB5, 0x85, 0xFF, 0xF6, 0x99, 0x49, 0xFF, 0xF5, 0x9B, 0x42, 0xFF, 0xF6, 0xA6, 0x4E, 0xFF, 0xF7, 0xB2, + 0x59, 0xFF, 0xF8, 0xBC, 0x65, 0xFF, 0xF9, 0xC6, 0x72, 0xFF, 0xF9, 0xD3, 0x7F, 0xFF, 0xFA, 0xE0, 0x8D, 0xFF, 0xF9, + 0xE5, 0x97, 0xFF, 0xF8, 0xEB, 0xA1, 0xFF, 0xFE, 0xEA, 0xA6, 0xFF, 0xFF, 0xEA, 0xAA, 0xFF, 0xFC, 0xEE, 0xA8, 0xFF, + 0xF9, 0xBA, 0x62, 0xFF, 0xFA, 0xDC, 0x98, 0xFF, 0xFE, 0xF3, 0xB9, 0xFF, 0xFB, 0xEC, 0xB2, 0xFF, 0xF7, 0xE5, 0xAB, + 0xFF, 0xFE, 0xE4, 0xA2, 0xFF, 0x64, 0xB0, 0xD1, 0xFF, 0x19, 0x9F, 0xF0, 0xFF, 0x26, 0x9E, 0xE8, 0xFF, 0x03, 0x98, + 0xF2, 0xFF, 0xE3, 0xEF, 0x50, 0xFF, 0xD5, 0xEE, 0x57, 0xFF, 0xBF, 0xE3, 0x64, 0xFF, 0xBC, 0xE1, 0x64, 0xFF, 0xB9, + 0xDF, 0x64, 0xFF, 0xB4, 0xDD, 0x5D, 0xFF, 0xB0, 0xDB, 0x56, 0xFF, 0xA9, 0xD7, 0x4E, 0xFF, 0xA2, 0xD3, 0x46, 0xFF, + 0x9B, 0xD0, 0x42, 0xFF, 0x93, 0xCD, 0x3F, 0xFF, 0x8B, 0xC9, 0x3D, 0xFF, 0x84, 0xC5, 0x3C, 0xFF, 0x80, 0xC1, 0x39, + 0xFF, 0x7D, 0xBC, 0x36, 0xFF, 0x8A, 0xC7, 0x45, 0xFF, 0x88, 0xC1, 0x44, 0xFF, 0x62, 0xA0, 0x2B, 0xFF, 0x64, 0xA9, + 0x2B, 0xFF, 0x5E, 0xA3, 0x2D, 0xFF, 0x4F, 0x95, 0x26, 0xFF, 0xA4, 0xCE, 0x98, 0xFF, 0xD8, 0xEA, 0xDC, 0xFF, 0xB9, + 0xDC, 0xFF, 0xFF, 0x38, 0x9D, 0xF3, 0xFF, 0x00, 0x8F, 0xD3, 0xFF, 0x45, 0xC1, 0xEF, 0xFF, 0x44, 0xC0, 0xEF, 0xFF, + 0x43, 0xBF, 0xEF, 0xFF, 0x41, 0xBE, 0xEF, 0xFF, 0x40, 0xBD, 0xEF, 0xFF, 0x41, 0xBC, 0xED, 0xFF, 0x42, 0xBA, 0xEB, + 0xFF, 0x3F, 0xBA, 0xEF, 0xFF, 0x3C, 0xB9, 0xF3, 0xFF, 0x34, 0xB8, 0xE6, 0xFF, 0x16, 0xBD, 0xF6, 0xFF, 0xD8, 0x7F, + 0x4F, 0xFF, 0xF7, 0x90, 0x46, 0xFF, 0xF7, 0xA5, 0x54, 0xFF, 0xFF, 0xDA, 0xBA, 0xFF, 0xF8, 0xA1, 0x4D, 0xFF, 0xF3, + 0xA5, 0x49, 0xFF, 0xF4, 0xAD, 0x53, 0xFF, 0xF6, 0xB5, 0x5D, 0xFF, 0xF8, 0xC0, 0x6E, 0xFF, 0xFA, 0xCC, 0x7F, 0xFF, + 0xF9, 0xD8, 0x8A, 0xFF, 0xF8, 0xE4, 0x95, 0xFF, 0xF8, 0xEC, 0xA1, 0xFF, 0xF7, 0xF4, 0xAE, 0xFF, 0xFE, 0xF3, 0xB2, + 0xFF, 0xFF, 0xF1, 0xB5, 0xFF, 0xFE, 0xF4, 0xAD, 0xFF, 0xF3, 0xB6, 0x59, 0xFF, 0xF8, 0xDA, 0x92, 0xFF, 0xFE, 0xFF, + 0xCC, 0xFF, 0xFA, 0xF6, 0xC1, 0xFF, 0xF7, 0xED, 0xB6, 0xFF, 0xFF, 0xF1, 0xAB, 0xFF, 0x13, 0xA4, 0xF7, 0xFF, 0x15, + 0xA4, 0xEF, 0xFF, 0x18, 0xA5, 0xE8, 0xFF, 0x56, 0xB4, 0xCD, 0xFF, 0xF0, 0xF2, 0x71, 0xFF, 0xD4, 0xEF, 0x84, 0xFF, + 0xCF, 0xE6, 0x7B, 0xFF, 0xCD, 0xE6, 0x7B, 0xFF, 0xCB, 0xE6, 0x7C, 0xFF, 0xC9, 0xE5, 0x71, 0xFF, 0xC6, 0xE5, 0x67, + 0xFF, 0xBC, 0xE1, 0x5F, 0xFF, 0xB1, 0xDD, 0x57, 0xFF, 0xA8, 0xDB, 0x51, 0xFF, 0xA0, 0xDA, 0x4B, 0xFF, 0x9C, 0xD7, + 0x48, 0xFF, 0x99, 0xD4, 0x46, 0xFF, 0x94, 0xCF, 0x42, 0xFF, 0x8F, 0xCA, 0x3E, 0xFF, 0x88, 0xC4, 0x3B, 0xFF, 0x81, + 0xBE, 0x39, 0xFF, 0x72, 0xB3, 0x30, 0xFF, 0x62, 0xA8, 0x27, 0xFF, 0x58, 0xA0, 0x27, 0xFF, 0x4E, 0x97, 0x27, 0xFF, + 0x9F, 0xC4, 0x79, 0xFF, 0xFF, 0xFB, 0xF7, 0xFF, 0x7F, 0xD2, 0xF4, 0xFF, 0x03, 0x8E, 0xE1, 0xFF, 0x0E, 0x89, 0xE1, + 0xFF, 0x47, 0xC3, 0xEF, 0xFF, 0x46, 0xC2, 0xEF, 0xFF, 0x44, 0xC0, 0xEF, 0xFF, 0x43, 0xBF, 0xEF, 0xFF, 0x41, 0xBE, + 0xF0, 0xFF, 0x42, 0xBD, 0xEE, 0xFF, 0x43, 0xBC, 0xEC, 0xFF, 0x40, 0xBC, 0xEF, 0xFF, 0x3E, 0xBB, 0xF1, 0xFF, 0x2F, + 0xC0, 0xFD, 0xFF, 0x35, 0xBD, 0xFB, 0xFF, 0xF5, 0x4B, 0x00, 0xFF, 0xFF, 0x8A, 0x52, 0xFF, 0xFA, 0xA5, 0x5D, 0xFF, + 0xFC, 0xC4, 0x8D, 0xFF, 0xFB, 0xC1, 0x85, 0xFF, 0xF5, 0xAD, 0x50, 0xFF, 0xF7, 0xB6, 0x5E, 0xFF, 0xF9, 0xBE, 0x6B, + 0xFF, 0xFA, 0xC9, 0x78, 0xFF, 0xFB, 0xD4, 0x85, 0xFF, 0xFE, 0xDE, 0x97, 0xFF, 0xFF, 0xE8, 0xAA, 0xFF, 0xFD, 0xEE, + 0xAD, 0xFF, 0xF9, 0xF4, 0xB1, 0xFF, 0xFC, 0xF5, 0xB9, 0xFF, 0xFE, 0xF6, 0xC2, 0xFF, 0xFB, 0xF0, 0xB2, 0xFF, 0xF6, + 0xCB, 0x6E, 0xFF, 0xFB, 0xDE, 0x91, 0xFF, 0xFC, 0xFC, 0xCA, 0xFF, 0xFF, 0xFB, 0xD0, 0xFF, 0xFF, 0xFC, 0xC8, 0xFF, + 0xCA, 0xE3, 0xC7, 0xFF, 0x15, 0xA1, 0xF2, 0xFF, 0x1D, 0xA3, 0xEE, 0xFF, 0x11, 0xA1, 0xF1, 0xFF, 0x9E, 0xD4, 0xB9, + 0xFF, 0xEA, 0xF1, 0x8B, 0xFF, 0xDC, 0xEF, 0x95, 0xFF, 0xD9, 0xEB, 0x90, 0xFF, 0xD9, 0xEB, 0x92, 0xFF, 0xD8, 0xEC, + 0x94, 0xFF, 0xD6, 0xEB, 0x8B, 0xFF, 0xD3, 0xEA, 0x82, 0xFF, 0xC9, 0xE6, 0x78, 0xFF, 0xBF, 0xE3, 0x6F, 0xFF, 0xB8, + 0xE2, 0x68, 0xFF, 0xB1, 0xE2, 0x61, 0xFF, 0xAE, 0xE0, 0x5D, 0xFF, 0xAC, 0xDE, 0x5A, 0xFF, 0xA2, 0xD9, 0x51, 0xFF, + 0x98, 0xD3, 0x48, 0xFF, 0x8E, 0xCB, 0x41, 0xFF, 0x83, 0xC3, 0x39, 0xFF, 0x74, 0xB7, 0x32, 0xFF, 0x66, 0xAC, 0x2C, + 0xFF, 0x5D, 0xA2, 0x29, 0xFF, 0x54, 0x99, 0x26, 0xFF, 0x4A, 0x93, 0x21, 0xFF, 0x23, 0x99, 0xB9, 0xFF, 0x15, 0x93, + 0xFE, 0xFF, 0x09, 0x92, 0xD8, 0xFF, 0x0F, 0x8F, 0xD8, 0xFF, 0x49, 0xC4, 0xEF, 0xFF, 0x47, 0xC3, 0xEF, 0xFF, 0x46, + 0xC2, 0xF0, 0xFF, 0x44, 0xC1, 0xF0, 0xFF, 0x42, 0xC0, 0xF1, 0xFF, 0x43, 0xBF, 0xEF, 0xFF, 0x43, 0xBE, 0xED, 0xFF, + 0x42, 0xBE, 0xEE, 0xFF, 0x41, 0xBD, 0xF0, 0xFF, 0x37, 0xBA, 0xF0, 0xFF, 0x71, 0xA1, 0xB7, 0xFF, 0xFE, 0x5D, 0x1D, + 0xFF, 0xF8, 0x79, 0x31, 0xFF, 0xF5, 0xA1, 0x51, 0xFF, 0xF8, 0xAD, 0x60, 0xFF, 0xFE, 0xE0, 0xBC, 0xFF, 0xF7, 0xB6, + 0x57, 0xFF, 0xF9, 0xBF, 0x68, 0xFF, 0xFC, 0xC8, 0x79, 0xFF, 0xFC, 0xD2, 0x82, 0xFF, 0xFC, 0xDB, 0x8B, 0xFF, 0xFB, + 0xDE, 0x8F, 0xFF, 0xFB, 0xE0, 0x92, 0xFF, 0xFA, 0xEA, 0xA3, 0xFF, 0xFA, 0xF4, 0xB4, 0xFF, 0xF9, 0xF8, 0xC1, 0xFF, + 0xF8, 0xFB, 0xCE, 0xFF, 0xF9, 0xEB, 0xB6, 0xFF, 0xFA, 0xE1, 0x83, 0xFF, 0xFD, 0xE2, 0x8F, 0xFF, 0xFB, 0xF9, 0xC7, + 0xFF, 0xFC, 0xF8, 0xD7, 0xFF, 0xFE, 0xFC, 0xCA, 0xFF, 0x8B, 0xCD, 0xDC, 0xFF, 0x18, 0x9F, 0xED, 0xFF, 0x24, 0xA3, + 0xED, 0xFF, 0x0A, 0x9D, 0xFA, 0xFF, 0xE7, 0xF5, 0xA5, 0xFF, 0xE4, 0xF1, 0xA5, 0xFF, 0xE4, 0xF0, 0xA5, 0xFF, 0xE3, + 0xEF, 0xA6, 0xFF, 0xE4, 0xF0, 0xA9, 0xFF, 0xE6, 0xF2, 0xAD, 0xFF, 0xE3, 0xF0, 0xA5, 0xFF, 0xE0, 0xEF, 0x9E, 0xFF, + 0xD6, 0xEC, 0x92, 0xFF, 0xCD, 0xE9, 0x87, 0xFF, 0xC7, 0xE9, 0x7F, 0xFF, 0xC2, 0xEA, 0x78, 0xFF, 0xC1, 0xEA, 0x72, + 0xFF, 0xC0, 0xE9, 0x6D, 0xFF, 0xB1, 0xE3, 0x60, 0xFF, 0xA2, 0xDD, 0x53, 0xFF, 0x94, 0xD2, 0x46, 0xFF, 0x86, 0xC8, + 0x3A, 0xFF, 0x77, 0xBC, 0x35, 0xFF, 0x69, 0xB0, 0x30, 0xFF, 0x62, 0xA5, 0x2B, 0xFF, 0x5B, 0x9B, 0x26, 0xFF, 0x57, + 0x91, 0x09, 0xFF, 0x09, 0x94, 0xFB, 0xFF, 0x0C, 0x95, 0xE5, 0xFF, 0x0F, 0x91, 0xEB, 0xFF, 0x0F, 0x91, 0xEB, 0xFF, + 0x4A, 0xC5, 0xEF, 0xFF, 0x48, 0xC4, 0xF0, 0xFF, 0x47, 0xC3, 0xF0, 0xFF, 0x45, 0xC2, 0xF1, 0xFF, 0x43, 0xC1, 0xF1, + 0xFF, 0x41, 0xC1, 0xF1, 0xFF, 0x3F, 0xC1, 0xF1, 0xFF, 0x3F, 0xBE, 0xF0, 0xFF, 0x3F, 0xBC, 0xEF, 0xFF, 0x32, 0xC2, + 0xFD, 0xFF, 0xBD, 0x7F, 0x6E, 0xFF, 0xFE, 0x65, 0x26, 0xFF, 0xF5, 0x7B, 0x34, 0xFF, 0xF5, 0x9A, 0x4C, 0xFF, 0xF8, + 0xAB, 0x5C, 0xFF, 0xFA, 0xD0, 0x9F, 0xFF, 0xF7, 0xC6, 0x83, 0xFF, 0xFD, 0xC1, 0x6A, 0xFF, 0xFD, 0xD1, 0x7E, 0xFF, + 0xFB, 0xDB, 0x87, 0xFF, 0xF9, 0xE5, 0x8F, 0xFF, 0xF8, 0xEC, 0x9A, 0xFF, 0xF7, 0xF4, 0xA5, 0xFF, 0xFB, 0xEA, 0x99, + 0xFF, 0xFF, 0xDF, 0x8E, 0xFF, 0xFB, 0xE2, 0x9F, 0xFF, 0xF7, 0xE6, 0xB1, 0xFF, 0xFB, 0xED, 0xCC, 0xFF, 0xFF, 0xFA, + 0xCA, 0xFF, 0xFF, 0xF2, 0xC6, 0xFF, 0xFC, 0xF0, 0xC2, 0xFF, 0xFE, 0xF5, 0xD2, 0xFF, 0xFF, 0xFC, 0xD3, 0xFF, 0x4B, + 0xB5, 0xE6, 0xFF, 0x20, 0xA4, 0xED, 0xFF, 0x1B, 0xA2, 0xED, 0xFF, 0x3D, 0xAA, 0xE2, 0xFF, 0xEE, 0xF6, 0xAB, 0xFF, + 0xE5, 0xF1, 0xB1, 0xFF, 0xE7, 0xF2, 0xB4, 0xFF, 0xE9, 0xF3, 0xB8, 0xFF, 0xE9, 0xF3, 0xBA, 0xFF, 0xEA, 0xF4, 0xBC, + 0xFF, 0xE8, 0xF3, 0xB5, 0xFF, 0xE5, 0xF2, 0xAF, 0xFF, 0xE0, 0xF0, 0xA8, 0xFF, 0xDA, 0xED, 0xA1, 0xFF, 0xD5, 0xEF, + 0x99, 0xFF, 0xD0, 0xF0, 0x91, 0xFF, 0xC8, 0xED, 0x82, 0xFF, 0xC0, 0xEA, 0x72, 0xFF, 0xB0, 0xE3, 0x61, 0xFF, 0xA0, + 0xDC, 0x50, 0xFF, 0x94, 0xD3, 0x47, 0xFF, 0x88, 0xCA, 0x3E, 0xFF, 0x7B, 0xBF, 0x38, 0xFF, 0x6E, 0xB4, 0x32, 0xFF, + 0x65, 0xA8, 0x2E, 0xFF, 0x5D, 0xA0, 0x1B, 0xFF, 0x3C, 0x94, 0x48, 0xFF, 0x0A, 0x93, 0xF6, 0xFF, 0x0D, 0x94, 0xEC, + 0xFF, 0x10, 0x92, 0xF0, 0xFF, 0x10, 0x92, 0xF0, 0xFF, 0x4B, 0xC5, 0xF0, 0xFF, 0x49, 0xC4, 0xF0, 0xFF, 0x48, 0xC4, + 0xF1, 0xFF, 0x46, 0xC3, 0xF1, 0xFF, 0x44, 0xC2, 0xF2, 0xFF, 0x3F, 0xC3, 0xF4, 0xFF, 0x3A, 0xC4, 0xF6, 0xFF, 0x3C, + 0xBF, 0xF3, 0xFF, 0x3D, 0xBA, 0xEF, 0xFF, 0x2C, 0xCA, 0xFF, 0xFF, 0xFF, 0x5D, 0x24, 0xFF, 0xFE, 0x6D, 0x2E, 0xFF, + 0xF2, 0x7D, 0x38, 0xFF, 0xF5, 0x93, 0x48, 0xFF, 0xF7, 0xA9, 0x57, 0xFF, 0xF7, 0xC0, 0x82, 0xFF, 0xF7, 0xD7, 0xAE, + 0xFF, 0xFF, 0xC2, 0x6C, 0xFF, 0xFE, 0xDA, 0x84, 0xFF, 0xFA, 0xE4, 0x8B, 0xFF, 0xF6, 0xEE, 0x93, 0xFF, 0xF8, 0xED, + 0x9D, 0xFF, 0xF9, 0xEC, 0xA7, 0xFF, 0xF8, 0xF1, 0xB3, 0xFF, 0xF7, 0xF6, 0xC0, 0xFF, 0xFB, 0xF6, 0xC8, 0xFF, 0xFF, + 0xF6, 0xD0, 0xFF, 0xFE, 0xF2, 0xD3, 0xFF, 0xFB, 0xF3, 0xB9, 0xFF, 0xFF, 0xFD, 0xE7, 0xFF, 0xF6, 0xFD, 0xE9, 0xFF, + 0xFC, 0xFC, 0xE2, 0xFF, 0xFF, 0xFC, 0xDC, 0xFF, 0x0B, 0x9D, 0xF1, 0xFF, 0x29, 0xAA, 0xEC, 0xFF, 0x1B, 0xAA, 0xF5, + 0xFF, 0x7F, 0xC7, 0xD9, 0xFF, 0xFD, 0xFE, 0xBA, 0xFF, 0xE7, 0xF2, 0xBD, 0xFF, 0xEB, 0xF4, 0xC3, 0xFF, 0xEE, 0xF6, + 0xCA, 0xFF, 0xEF, 0xF6, 0xCA, 0xFF, 0xEF, 0xF7, 0xCB, 0xFF, 0xED, 0xF6, 0xC5, 0xFF, 0xEB, 0xF5, 0xBF, 0xFF, 0xE9, + 0xF3, 0xBE, 0xFF, 0xE8, 0xF2, 0xBC, 0xFF, 0xE3, 0xF4, 0xB3, 0xFF, 0xDF, 0xF6, 0xAB, 0xFF, 0xD0, 0xF1, 0x91, 0xFF, + 0xC1, 0xEC, 0x77, 0xFF, 0xAF, 0xE3, 0x62, 0xFF, 0x9E, 0xDB, 0x4E, 0xFF, 0x94, 0xD3, 0x47, 0xFF, 0x8A, 0xCC, 0x41, + 0xFF, 0x7F, 0xC2, 0x3B, 0xFF, 0x73, 0xB8, 0x35, 0xFF, 0x69, 0xAC, 0x30, 0xFF, 0x60, 0xA5, 0x10, 0xFF, 0x22, 0x96, + 0x86, 0xFF, 0x0A, 0x91, 0xF0, 0xFF, 0x0E, 0x92, 0xF2, 0xFF, 0x11, 0x94, 0xF4, 0xFF, 0x11, 0x94, 0xF4, 0xFF, 0x4C, + 0xC5, 0xF1, 0xFF, 0x4A, 0xC5, 0xF1, 0xFF, 0x49, 0xC4, 0xF1, 0xFF, 0x47, 0xC4, 0xF2, 0xFF, 0x45, 0xC3, 0xF2, 0xFF, + 0x43, 0xC3, 0xF1, 0xFF, 0x40, 0xC4, 0xF0, 0xFF, 0x42, 0xBF, 0xF3, 0xFF, 0x39, 0xC0, 0xF5, 0xFF, 0x5E, 0xAC, 0xCA, + 0xFF, 0xFA, 0x58, 0x1E, 0xFF, 0xF3, 0x6E, 0x30, 0xFF, 0xF7, 0x80, 0x35, 0xFF, 0xFB, 0x92, 0x3E, 0xFF, 0xFB, 0xAF, + 0x5D, 0xFF, 0xFF, 0xC2, 0x72, 0xFF, 0xFD, 0xE1, 0xBA, 0xFF, 0xFF, 0xCD, 0x74, 0xFF, 0xFF, 0xD3, 0x71, 0xFF, 0xFF, + 0xE5, 0x83, 0xFF, 0xFF, 0xF7, 0x95, 0xFF, 0xFE, 0xF4, 0xA1, 0xFF, 0xFD, 0xF0, 0xAD, 0xFF, 0xFF, 0xF8, 0xC1, 0xFF, + 0xFB, 0xF7, 0xCD, 0xFF, 0xFE, 0xF8, 0xD1, 0xFF, 0xFF, 0xF9, 0xD6, 0xFF, 0xFE, 0xF6, 0xE0, 0xFF, 0xFB, 0xF5, 0xDD, + 0xFF, 0xFF, 0xFB, 0xED, 0xFF, 0xFB, 0xFB, 0xE8, 0xFF, 0xFF, 0xFC, 0xDF, 0xFF, 0xB2, 0xE0, 0xE8, 0xFF, 0x18, 0xA3, + 0xEF, 0xFF, 0x25, 0xAA, 0xEC, 0xFF, 0x15, 0xA8, 0xF5, 0xFF, 0xC2, 0xE3, 0xD8, 0xFF, 0xF9, 0xF9, 0xC5, 0xFF, 0xEE, + 0xF5, 0xCA, 0xFF, 0xEF, 0xF6, 0xCE, 0xFF, 0xF0, 0xF7, 0xD2, 0xFF, 0xF1, 0xF8, 0xD1, 0xFF, 0xF1, 0xF9, 0xD0, 0xFF, + 0xF1, 0xF9, 0xCD, 0xFF, 0xF1, 0xF9, 0xC9, 0xFF, 0xF2, 0xFB, 0xC9, 0xFF, 0xF4, 0xFC, 0xCA, 0xFF, 0xE6, 0xF8, 0xB6, + 0xFF, 0xD9, 0xF3, 0xA2, 0xFF, 0xCA, 0xEF, 0x89, 0xFF, 0xBC, 0xEB, 0x71, 0xFF, 0xB0, 0xE6, 0x61, 0xFF, 0xA4, 0xE1, + 0x50, 0xFF, 0x99, 0xD9, 0x48, 0xFF, 0x8F, 0xD2, 0x40, 0xFF, 0x83, 0xC7, 0x3A, 0xFF, 0x77, 0xBC, 0x34, 0xFF, 0x6A, + 0xB2, 0x1C, 0xFF, 0x5D, 0xA9, 0x04, 0xFF, 0x13, 0x8D, 0xEA, 0xFF, 0x11, 0x93, 0xEF, 0xFF, 0x0F, 0x92, 0xEF, 0xFF, + 0x0E, 0x92, 0xF0, 0xFF, 0x0E, 0x92, 0xF0, 0xFF, 0x4D, 0xC6, 0xF2, 0xFF, 0x4B, 0xC5, 0xF2, 0xFF, 0x4A, 0xC5, 0xF2, + 0xFF, 0x48, 0xC5, 0xF2, 0xFF, 0x46, 0xC4, 0xF2, 0xFF, 0x46, 0xC4, 0xEE, 0xFF, 0x46, 0xC4, 0xEA, 0xFF, 0x48, 0xBF, + 0xF2, 0xFF, 0x34, 0xC6, 0xFB, 0xFF, 0x98, 0x95, 0x91, 0xFF, 0xFC, 0x64, 0x27, 0xFF, 0xF1, 0x76, 0x3B, 0xFF, 0xFC, + 0x83, 0x32, 0xFF, 0xFF, 0x91, 0x34, 0xFF, 0xFF, 0xB4, 0x63, 0xFF, 0xFF, 0xBD, 0x5A, 0xFF, 0xF3, 0xDC, 0xB5, 0xFF, + 0xCB, 0xD0, 0x97, 0xFF, 0xB4, 0xCE, 0xA4, 0xFF, 0xAF, 0xD2, 0xB0, 0xFF, 0xAB, 0xD6, 0xBC, 0xFF, 0xC2, 0xE1, 0xBE, + 0xFF, 0xDA, 0xEB, 0xC0, 0xFF, 0xF5, 0xFC, 0xC7, 0xFF, 0xFF, 0xFE, 0xBD, 0xFF, 0xFF, 0xFD, 0xCC, 0xFF, 0xFF, 0xFC, + 0xDB, 0xFF, 0xFE, 0xFC, 0xE0, 0xFF, 0xFB, 0xFC, 0xE4, 0xFF, 0xFD, 0xFB, 0xE6, 0xFF, 0xFF, 0xFA, 0xE7, 0xFF, 0xFF, + 0xFB, 0xDD, 0xFF, 0x61, 0xC4, 0xF4, 0xFF, 0x26, 0xAA, 0xEE, 0xFF, 0x22, 0xAA, 0xEB, 0xFF, 0x10, 0xA7, 0xF6, 0xFF, + 0xFF, 0xFF, 0xD6, 0xFF, 0xF5, 0xF4, 0xCF, 0xFF, 0xF5, 0xF9, 0xD8, 0xFF, 0xF4, 0xF9, 0xD9, 0xFF, 0xF2, 0xF8, 0xD9, + 0xFF, 0xF3, 0xF9, 0xD8, 0xFF, 0xF4, 0xFB, 0xD6, 0xFF, 0xF5, 0xFC, 0xD5, 0xFF, 0xF6, 0xFD, 0xD3, 0xFF, 0xF3, 0xFA, + 0xCD, 0xFF, 0xEF, 0xF6, 0xC7, 0xFF, 0xE1, 0xF3, 0xB0, 0xFF, 0xD3, 0xF0, 0x98, 0xFF, 0xC5, 0xED, 0x82, 0xFF, 0xB7, + 0xEB, 0x6B, 0xFF, 0xB0, 0xE9, 0x5F, 0xFF, 0xAA, 0xE7, 0x53, 0xFF, 0x9F, 0xDF, 0x49, 0xFF, 0x93, 0xD7, 0x3E, 0xFF, + 0x87, 0xCB, 0x39, 0xFF, 0x7B, 0xBF, 0x34, 0xFF, 0x6B, 0xB4, 0x25, 0xFF, 0x5B, 0xA2, 0x32, 0xFF, 0x04, 0x94, 0xF9, + 0xFF, 0x17, 0x94, 0xED, 0xFF, 0x11, 0x92, 0xEC, 0xFF, 0x0B, 0x91, 0xEB, 0xFF, 0x0B, 0x91, 0xEB, 0xFF, 0x4E, 0xC7, + 0xF2, 0xFF, 0x4D, 0xC7, 0xF3, 0xFF, 0x4C, 0xC7, 0xF3, 0xFF, 0x4A, 0xC7, 0xF3, 0xFF, 0x49, 0xC7, 0xF4, 0xFF, 0x47, + 0xC4, 0xF1, 0xFF, 0x45, 0xC1, 0xEE, 0xFF, 0x42, 0xC2, 0xF7, 0xFF, 0x33, 0xC8, 0xFF, 0xFF, 0xDE, 0x67, 0x46, 0xFF, + 0xFF, 0x63, 0x2A, 0xFF, 0xFF, 0x6F, 0x1B, 0xFF, 0xE0, 0x8B, 0x52, 0xFF, 0xA3, 0xA0, 0x84, 0xFF, 0x62, 0xC1, 0xCC, + 0xFF, 0x26, 0xC0, 0xFF, 0xFF, 0x29, 0xB7, 0xFF, 0xFF, 0x24, 0xB5, 0xF1, 0xFF, 0x27, 0xB7, 0xF9, 0xFF, 0x25, 0xB5, + 0xF6, 0xFF, 0x23, 0xB2, 0xF2, 0xFF, 0x24, 0xB5, 0xFA, 0xFF, 0x24, 0xB7, 0xFF, 0xFF, 0x17, 0x9D, 0xDE, 0xFF, 0x42, + 0xBA, 0xF4, 0xFF, 0x9E, 0xDA, 0xE7, 0xFF, 0xF9, 0xF9, 0xDC, 0xFF, 0xF3, 0xFB, 0xE6, 0xFF, 0xFF, 0xFF, 0xE9, 0xFF, + 0xFD, 0xFF, 0xE6, 0xFF, 0xFA, 0xFB, 0xE2, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1D, 0xA7, 0xEF, 0xFF, 0x1C, 0xA7, 0xF0, + 0xFF, 0x1A, 0xA7, 0xF1, 0xFF, 0x5A, 0xC4, 0xF0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0xFA, 0xF9, 0xE1, 0xFF, 0xFA, 0xFB, + 0xE2, 0xFF, 0xF8, 0xFB, 0xDF, 0xFF, 0xF5, 0xFA, 0xDC, 0xFF, 0xF5, 0xFB, 0xDB, 0xFF, 0xF5, 0xFB, 0xD9, 0xFF, 0xF5, + 0xFC, 0xD6, 0xFF, 0xF5, 0xFD, 0xD3, 0xFF, 0xF0, 0xF8, 0xC8, 0xFF, 0xEA, 0xF4, 0xBD, 0xFF, 0xDF, 0xF1, 0xA8, 0xFF, + 0xD4, 0xEF, 0x93, 0xFF, 0xC6, 0xF3, 0x7A, 0xFF, 0xB9, 0xF8, 0x61, 0xFF, 0xB0, 0xEF, 0x57, 0xFF, 0xA6, 0xE6, 0x4D, + 0xFF, 0xA3, 0xE2, 0x48, 0xFF, 0x98, 0xD6, 0x3A, 0xFF, 0x89, 0xCD, 0x37, 0xFF, 0x7B, 0xC3, 0x35, 0xFF, 0x6F, 0xB7, + 0x20, 0xFF, 0x3A, 0x9C, 0x84, 0xFF, 0x0C, 0x93, 0xF4, 0xFF, 0x13, 0x94, 0xEC, 0xFF, 0x11, 0x93, 0xE9, 0xFF, 0x0F, + 0x92, 0xE6, 0xFF, 0x0F, 0x92, 0xE6, 0xFF, 0x50, 0xC9, 0xF3, 0xFF, 0x4F, 0xC9, 0xF4, 0xFF, 0x4E, 0xC9, 0xF4, 0xFF, + 0x4D, 0xCA, 0xF5, 0xFF, 0x4B, 0xCA, 0xF5, 0xFF, 0x48, 0xC5, 0xF4, 0xFF, 0x44, 0xBF, 0xF3, 0xFF, 0x47, 0xC1, 0xEE, + 0xFF, 0x4A, 0xC4, 0xEA, 0xFF, 0xFF, 0x52, 0x1F, 0xFF, 0xA6, 0x9A, 0x92, 0xFF, 0x51, 0xB6, 0xE6, 0xFF, 0x28, 0xC7, + 0xFF, 0xFF, 0x2C, 0xC4, 0xF8, 0xFF, 0x30, 0xC0, 0xF0, 0xFF, 0x3F, 0xBA, 0xEF, 0xFF, 0x37, 0xBF, 0xEF, 0xFF, 0x38, + 0xB9, 0xEF, 0xFF, 0x3A, 0xB2, 0xF0, 0xFF, 0x38, 0xB5, 0xF3, 0xFF, 0x35, 0xB7, 0xF6, 0xFF, 0x32, 0xB9, 0xEF, 0xFF, + 0x2F, 0xBB, 0xE8, 0xFF, 0x2F, 0xB8, 0xEA, 0xFF, 0x2F, 0xB4, 0xED, 0xFF, 0x1F, 0xAC, 0xF3, 0xFF, 0x10, 0xA3, 0xF9, + 0xFF, 0x6F, 0xC9, 0xF2, 0xFF, 0xF5, 0xF9, 0xDF, 0xFF, 0xF5, 0xFB, 0xDE, 0xFF, 0xF5, 0xFD, 0xDD, 0xFF, 0xD7, 0xEA, + 0xE3, 0xFF, 0x10, 0xA5, 0xEE, 0xFF, 0x2D, 0xB2, 0xF4, 0xFF, 0x13, 0xA5, 0xF7, 0xFF, 0xA5, 0xE1, 0xEB, 0xFF, 0xFF, + 0xFF, 0xF8, 0xFF, 0xFF, 0xFE, 0xF2, 0xFF, 0xFF, 0xFD, 0xEC, 0xFF, 0xFC, 0xFC, 0xE6, 0xFF, 0xF7, 0xFC, 0xDF, 0xFF, + 0xF7, 0xFC, 0xDE, 0xFF, 0xF6, 0xFC, 0xDC, 0xFF, 0xF5, 0xFC, 0xD7, 0xFF, 0xF4, 0xFC, 0xD3, 0xFF, 0xED, 0xF7, 0xC3, + 0xFF, 0xE5, 0xF1, 0xB4, 0xFF, 0xE4, 0xF5, 0xB7, 0xFF, 0xE4, 0xF9, 0xBB, 0xFF, 0xEB, 0xFE, 0xD2, 0xFF, 0xF2, 0xFF, + 0xE9, 0xFF, 0xED, 0xFE, 0xDB, 0xFF, 0xE8, 0xF9, 0xCD, 0xFF, 0xCA, 0xEF, 0x89, 0xFF, 0x9C, 0xD6, 0x35, 0xFF, 0x83, + 0xC6, 0x2D, 0xFF, 0x6B, 0xB7, 0x25, 0xFF, 0x6C, 0xB3, 0x14, 0xFF, 0x1A, 0x95, 0xD6, 0xFF, 0x15, 0x91, 0xEE, 0xFF, + 0x0F, 0x93, 0xEB, 0xFF, 0x10, 0x93, 0xE6, 0xFF, 0x12, 0x93, 0xE0, 0xFF, 0x12, 0x93, 0xE0, 0xFF, 0x52, 0xCA, 0xF4, + 0xFF, 0x50, 0xCA, 0xF4, 0xFF, 0x4E, 0xCA, 0xF3, 0xFF, 0x4C, 0xC9, 0xF3, 0xFF, 0x4A, 0xC9, 0xF3, 0xFF, 0x48, 0xC8, + 0xF4, 0xFF, 0x46, 0xC6, 0xF6, 0xFF, 0x3F, 0xBF, 0xEC, 0xFF, 0x41, 0xBF, 0xEB, 0xFF, 0x40, 0xD4, 0xF8, 0xFF, 0x33, + 0xC9, 0xFC, 0xFF, 0x2F, 0xC9, 0xFF, 0xFF, 0x42, 0xC2, 0xEC, 0xFF, 0x40, 0xC3, 0xF4, 0xFF, 0x3E, 0xC3, 0xFC, 0xFF, + 0x34, 0xBB, 0xF3, 0xFF, 0x33, 0xBB, 0xF2, 0xFF, 0x49, 0xBD, 0xF6, 0xFF, 0x38, 0xB7, 0xF8, 0xFF, 0x36, 0xB7, 0xF5, + 0xFF, 0x34, 0xB7, 0xF2, 0xFF, 0x2E, 0xB5, 0xF3, 0xFF, 0x27, 0xB3, 0xF5, 0xFF, 0x2F, 0xBA, 0xF7, 0xFF, 0x2F, 0xBA, + 0xF2, 0xFF, 0x30, 0xB5, 0xF1, 0xFF, 0x31, 0xB0, 0xF0, 0xFF, 0x1E, 0xAC, 0xF6, 0xFF, 0x0C, 0xAA, 0xED, 0xFF, 0x7E, + 0xD2, 0xEC, 0xFF, 0xFF, 0xFF, 0xE6, 0xFF, 0x80, 0xD9, 0xD2, 0xFF, 0x2E, 0xA9, 0xF8, 0xFF, 0x1C, 0xAF, 0xEB, 0xFF, + 0x02, 0xAA, 0xE5, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFE, 0xF9, 0xFF, 0xFF, 0xFD, 0xF4, + 0xFF, 0xFD, 0xFD, 0xEB, 0xFF, 0xFA, 0xFE, 0xE2, 0xFF, 0xF9, 0xFD, 0xE1, 0xFF, 0xF7, 0xFC, 0xE0, 0xFF, 0xF5, 0xFC, + 0xD7, 0xFF, 0xF3, 0xFD, 0xCF, 0xFF, 0xF4, 0xFB, 0xE2, 0xFF, 0xF6, 0xFD, 0xE7, 0xFF, 0xF3, 0xFD, 0xE8, 0xFF, 0xF0, + 0xFD, 0xE9, 0xFF, 0xEB, 0xFD, 0xD3, 0xFF, 0xE5, 0xFC, 0xBD, 0xFF, 0xDF, 0xF7, 0xBA, 0xFF, 0xDA, 0xF2, 0xB6, 0xFF, + 0xE9, 0xFB, 0xD2, 0xFF, 0xF1, 0xFC, 0xE6, 0xFF, 0xB6, 0xDE, 0x8D, 0xFF, 0x84, 0xC7, 0x3C, 0xFF, 0x99, 0xB7, 0x47, + 0xFF, 0x13, 0xA1, 0xF8, 0xFF, 0x04, 0x94, 0xF2, 0xFF, 0x10, 0x94, 0xEE, 0xFF, 0x10, 0x94, 0xEC, 0xFF, 0x10, 0x95, + 0xE9, 0xFF, 0x10, 0x95, 0xE9, 0xFF, 0x53, 0xCC, 0xF5, 0xFF, 0x50, 0xCB, 0xF3, 0xFF, 0x4E, 0xCA, 0xF2, 0xFF, 0x4B, + 0xC9, 0xF1, 0xFF, 0x48, 0xC7, 0xF0, 0xFF, 0x48, 0xCB, 0xF4, 0xFF, 0x47, 0xCE, 0xF9, 0xFF, 0x40, 0xC4, 0xF2, 0xFF, + 0x48, 0xCA, 0xFC, 0xFF, 0x3F, 0xC2, 0xF0, 0xFF, 0x46, 0xC9, 0xF5, 0xFF, 0x46, 0xC7, 0xF4, 0xFF, 0x45, 0xC4, 0xF3, + 0xFF, 0x38, 0xB4, 0xED, 0xFF, 0x2C, 0xA5, 0xE8, 0xFF, 0x2E, 0xB0, 0xE1, 0xFF, 0x56, 0xC0, 0xEA, 0xFF, 0x6C, 0xC8, + 0xE9, 0xFF, 0x36, 0xC1, 0xE4, 0xFF, 0x50, 0xC9, 0xEB, 0xFF, 0x6A, 0xD1, 0xF1, 0xFF, 0x73, 0xD0, 0xF5, 0xFF, 0x7D, + 0xCF, 0xF9, 0xFF, 0x56, 0xC7, 0xF8, 0xFF, 0x1F, 0xAF, 0xE7, 0xFF, 0x25, 0xB1, 0xED, 0xFF, 0x2B, 0xB2, 0xF4, 0xFF, + 0x3E, 0xB5, 0xF9, 0xFF, 0x2A, 0xB3, 0xEE, 0xFF, 0x1B, 0xAF, 0xF5, 0xFF, 0x32, 0xB5, 0xF0, 0xFF, 0x3F, 0xB1, 0xF9, + 0xFF, 0x26, 0xA9, 0xF2, 0xFF, 0x1F, 0xAE, 0xEA, 0xFF, 0x3F, 0xB8, 0xF3, 0xFF, 0xFB, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xFB, 0xFF, 0xFE, 0xFE, 0xF0, 0xFF, 0xFD, 0xFF, 0xE5, 0xFF, 0xFB, + 0xFE, 0xE4, 0xFF, 0xF8, 0xFC, 0xE3, 0xFF, 0xF5, 0xFD, 0xD7, 0xFF, 0xF2, 0xFD, 0xCB, 0xFF, 0xF4, 0xFB, 0xEB, 0xFF, + 0xF6, 0xFE, 0xEE, 0xFF, 0xF1, 0xFD, 0xDE, 0xFF, 0xED, 0xFB, 0xCE, 0xFF, 0xE2, 0xF9, 0xB0, 0xFF, 0xD8, 0xF6, 0x91, + 0xFF, 0xD2, 0xF3, 0x8A, 0xFF, 0xCC, 0xF1, 0x83, 0xFF, 0xCE, 0xEE, 0x96, 0xFF, 0xD0, 0xEA, 0xA9, 0xFF, 0xDA, 0xEA, + 0xC0, 0xFF, 0xF4, 0xFA, 0xE8, 0xFF, 0x7E, 0xC6, 0x78, 0xFF, 0x59, 0xC0, 0xFF, 0xFF, 0x19, 0xA0, 0xEA, 0xFF, 0x10, + 0x95, 0xF2, 0xFF, 0x0F, 0x96, 0xF2, 0xFF, 0x0D, 0x96, 0xF2, 0xFF, 0x0D, 0x96, 0xF2, 0xFF, 0x54, 0xCD, 0xF4, 0xFF, + 0x51, 0xCB, 0xF4, 0xFF, 0x4F, 0xCA, 0xF3, 0xFF, 0x4C, 0xC9, 0xF2, 0xFF, 0x4A, 0xC8, 0xF2, 0xFF, 0x48, 0xC6, 0xF1, + 0xFF, 0x47, 0xC4, 0xF1, 0xFF, 0x48, 0xD2, 0xF3, 0xFF, 0x46, 0xC7, 0xF3, 0xFF, 0x4C, 0xC5, 0xFB, 0xFF, 0x2B, 0x9A, + 0xDC, 0xFF, 0x17, 0x83, 0xCD, 0xFF, 0x03, 0x6B, 0xBE, 0xFF, 0x00, 0x7F, 0xC5, 0xFF, 0x0E, 0x96, 0xD4, 0xFF, 0x2E, + 0xAC, 0xDB, 0xFF, 0x60, 0xC5, 0xEA, 0xFF, 0x75, 0xCC, 0xEF, 0xFF, 0x51, 0xCA, 0xEA, 0xFF, 0x69, 0xD2, 0xEF, 0xFF, + 0x81, 0xDA, 0xF5, 0xFF, 0x99, 0xE4, 0xF7, 0xFF, 0xB2, 0xEE, 0xF9, 0xFF, 0xCE, 0xFA, 0xFF, 0xFF, 0xE2, 0xFE, 0xFF, + 0xFF, 0x99, 0xE1, 0xFF, 0xFF, 0x48, 0xBC, 0xF7, 0xFF, 0x10, 0xB4, 0xDC, 0xFF, 0x31, 0xAD, 0xF0, 0xFF, 0x27, 0xAC, + 0xFB, 0xFF, 0x30, 0xB2, 0xF3, 0xFF, 0x34, 0xB1, 0xF5, 0xFF, 0x24, 0xAD, 0xF0, 0xFF, 0x26, 0xAC, 0xF6, 0xFF, 0x97, + 0xD1, 0xFC, 0xFF, 0xFF, 0xFD, 0xF7, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFB, 0xFF, 0xFE, 0xFF, 0xF3, 0xFF, + 0xFD, 0xFF, 0xED, 0xFF, 0xFC, 0xFD, 0xE7, 0xFF, 0xFB, 0xFE, 0xE3, 0xFF, 0xF9, 0xFE, 0xDF, 0xFF, 0xF8, 0xFD, 0xE7, + 0xFF, 0xF7, 0xFC, 0xEF, 0xFF, 0xF3, 0xFB, 0xEB, 0xFF, 0xEF, 0xFD, 0xD8, 0xFF, 0xE8, 0xFA, 0xC2, 0xFF, 0xE2, 0xF8, + 0xAB, 0xFF, 0xD8, 0xF4, 0x9B, 0xFF, 0xCE, 0xEF, 0x8A, 0xFF, 0xC1, 0xEA, 0x76, 0xFF, 0xB4, 0xE5, 0x61, 0xFF, 0xAB, + 0xDD, 0x5A, 0xFF, 0xA2, 0xD2, 0x61, 0xFF, 0xC1, 0xE9, 0x8D, 0xFF, 0xDA, 0xE7, 0xB8, 0xFF, 0x96, 0xD4, 0xFF, 0xFF, + 0x8E, 0xD0, 0xFA, 0xFF, 0x41, 0xAD, 0xED, 0xFF, 0x10, 0x95, 0xF1, 0xFF, 0x0F, 0x95, 0xF1, 0xFF, 0x0E, 0x96, 0xF1, + 0xFF, 0x0E, 0x96, 0xF1, 0xFF, 0x54, 0xCD, 0xF4, 0xFF, 0x52, 0xCC, 0xF4, 0xFF, 0x50, 0xCB, 0xF4, 0xFF, 0x4E, 0xC9, + 0xF3, 0xFF, 0x4B, 0xC8, 0xF3, 0xFF, 0x51, 0xC9, 0xF6, 0xFF, 0x56, 0xCA, 0xFA, 0xFF, 0x44, 0xC0, 0xEA, 0xFF, 0x19, + 0x74, 0xC6, 0xFF, 0x00, 0x58, 0xAD, 0xFF, 0x01, 0x5B, 0xB3, 0xFF, 0x06, 0x6F, 0xC0, 0xFF, 0x0B, 0x84, 0xCC, 0xFF, + 0x00, 0x93, 0xCE, 0xFF, 0x11, 0xA7, 0xDF, 0xFF, 0x3E, 0xB9, 0xE5, 0xFF, 0x6A, 0xCA, 0xEB, 0xFF, 0x7E, 0xD1, 0xF5, + 0xFF, 0x6B, 0xD3, 0xF0, 0xFF, 0x81, 0xDB, 0xF4, 0xFF, 0x97, 0xE3, 0xF8, 0xFF, 0xA4, 0xEB, 0xF7, 0xFF, 0xB1, 0xF4, + 0xF5, 0xFF, 0xC7, 0xF7, 0xF9, 0xFF, 0xDC, 0xFA, 0xFC, 0xFF, 0xF2, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xF5, 0xFF, 0xBB, + 0xEB, 0xFD, 0xFF, 0x22, 0xB4, 0xF2, 0xFF, 0x28, 0xAF, 0xFF, 0xFF, 0x2F, 0xB0, 0xF6, 0xFF, 0x29, 0xB0, 0xF2, 0xFF, + 0x22, 0xB1, 0xEE, 0xFF, 0x19, 0xA7, 0xF9, 0xFF, 0xC9, 0xE6, 0xF4, 0xFF, 0xF7, 0xF7, 0xF4, 0xFF, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFE, 0xFF, 0xF6, 0xFF, 0xFD, 0xFF, 0xEC, 0xFF, 0xFC, 0xFF, 0xEA, 0xFF, 0xFA, 0xFA, 0xE8, 0xFF, 0xFB, 0xFD, + 0xE2, 0xFF, 0xFB, 0xFF, 0xDC, 0xFF, 0xFB, 0xFF, 0xE9, 0xFF, 0xFB, 0xFF, 0xF6, 0xFF, 0xF1, 0xFD, 0xDC, 0xFF, 0xE7, + 0xFB, 0xC3, 0xFF, 0xDF, 0xF5, 0xB4, 0xFF, 0xD8, 0xF0, 0xA5, 0xFF, 0xCE, 0xEC, 0x94, 0xFF, 0xC4, 0xE8, 0x83, 0xFF, + 0xB7, 0xE5, 0x77, 0xFF, 0xAB, 0xE3, 0x6B, 0xFF, 0xA0, 0xDE, 0x52, 0xFF, 0x94, 0xD4, 0x55, 0xFF, 0x7F, 0xBD, 0x40, + 0xFF, 0xD1, 0xE4, 0x98, 0xFF, 0x2B, 0xA1, 0xF4, 0xFF, 0x2F, 0xA1, 0xF6, 0xFF, 0x1F, 0x9B, 0xF3, 0xFF, 0x0F, 0x95, + 0xF0, 0xFF, 0x0F, 0x95, 0xF0, 0xFF, 0x0F, 0x95, 0xF0, 0xFF, 0x0F, 0x95, 0xF0, 0xFF, 0x55, 0xCE, 0xF4, 0xFF, 0x53, + 0xCC, 0xF4, 0xFF, 0x51, 0xCB, 0xF4, 0xFF, 0x4F, 0xCA, 0xF5, 0xFF, 0x4E, 0xC9, 0xF6, 0xFF, 0x4D, 0xC9, 0xF4, 0xFF, + 0x53, 0xD0, 0xFA, 0xFF, 0x2A, 0x86, 0xCD, 0xFF, 0x06, 0x52, 0xB0, 0xFF, 0x04, 0x5F, 0xB8, 0xFF, 0x0A, 0x73, 0xC8, + 0xFF, 0x08, 0x82, 0xCE, 0xFF, 0x06, 0x91, 0xD3, 0xFF, 0x01, 0xA0, 0xD5, 0xFF, 0x24, 0xB4, 0xE6, 0xFF, 0x4C, 0xC4, + 0xEA, 0xFF, 0x74, 0xD3, 0xED, 0xFF, 0x83, 0xD9, 0xF4, 0xFF, 0x7E, 0xDC, 0xF3, 0xFF, 0x93, 0xE4, 0xF6, 0xFF, 0xA8, + 0xEC, 0xF8, 0xFF, 0xB5, 0xF2, 0xF9, 0xFF, 0xC3, 0xF8, 0xF9, 0xFF, 0xD3, 0xFA, 0xFA, 0xFF, 0xE2, 0xFB, 0xFB, 0xFF, + 0xED, 0xFE, 0xFB, 0xFF, 0xEF, 0xF9, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0x7E, 0xDC, 0xEE, + 0xFF, 0x26, 0xAD, 0xFD, 0xFF, 0x29, 0xAF, 0xF7, 0xFF, 0x2D, 0xB1, 0xF1, 0xFF, 0x34, 0xB1, 0xDF, 0xFF, 0x09, 0xA6, + 0xF6, 0xFF, 0x8C, 0xD3, 0xF4, 0xFF, 0xFC, 0xFB, 0xF8, 0xFF, 0xFF, 0xFF, 0xF6, 0xFF, 0xFD, 0xFF, 0xEB, 0xFF, 0xFC, + 0xFE, 0xE5, 0xFF, 0xFB, 0xFB, 0xE0, 0xFF, 0xF9, 0xFC, 0xDE, 0xFF, 0xF7, 0xFC, 0xDC, 0xFF, 0xFC, 0xFF, 0xEF, 0xFF, + 0xF8, 0xFC, 0xEB, 0xFF, 0xE8, 0xF5, 0xD0, 0xFF, 0xDF, 0xF5, 0xBC, 0xFF, 0xD8, 0xF1, 0xAC, 0xFF, 0xD2, 0xED, 0x9D, + 0xFF, 0xC4, 0xE8, 0x7D, 0xFF, 0xB7, 0xE1, 0x6C, 0xFF, 0xAB, 0xDC, 0x5E, 0xFF, 0x9E, 0xD7, 0x4F, 0xFF, 0x98, 0xC9, + 0x5E, 0xFF, 0x92, 0xC6, 0x35, 0xFF, 0x8B, 0xC9, 0x42, 0xFF, 0x80, 0xB2, 0x4D, 0xFF, 0x00, 0x9B, 0xF1, 0xFF, 0x17, + 0x93, 0xF8, 0xFF, 0x15, 0x95, 0xF4, 0xFF, 0x12, 0x97, 0xF1, 0xFF, 0x11, 0x96, 0xF0, 0xFF, 0x10, 0x95, 0xEF, 0xFF, + 0x10, 0x95, 0xEF, 0xFF, 0x55, 0xCE, 0xF4, 0xFF, 0x54, 0xCD, 0xF4, 0xFF, 0x52, 0xCC, 0xF5, 0xFF, 0x51, 0xCB, 0xF6, + 0xFF, 0x50, 0xCB, 0xF8, 0xFF, 0x49, 0xC8, 0xF1, 0xFF, 0x51, 0xD5, 0xF9, 0xFF, 0x15, 0x62, 0xC0, 0xFF, 0x00, 0x5C, + 0xBB, 0xFF, 0x07, 0x74, 0xCC, 0xFF, 0x02, 0x7C, 0xCD, 0xFF, 0x02, 0x8D, 0xD4, 0xFF, 0x01, 0x9E, 0xDB, 0xFF, 0x08, + 0xAD, 0xDC, 0xFF, 0x36, 0xC1, 0xED, 0xFF, 0x5A, 0xCF, 0xEE, 0xFF, 0x7D, 0xDC, 0xF0, 0xFF, 0x87, 0xE1, 0xF3, 0xFF, + 0x91, 0xE6, 0xF7, 0xFF, 0xA5, 0xED, 0xF8, 0xFF, 0xB8, 0xF5, 0xF8, 0xFF, 0xC6, 0xF9, 0xFB, 0xFF, 0xD4, 0xFD, 0xFD, + 0xFF, 0xDF, 0xFC, 0xFB, 0xFF, 0xE9, 0xFC, 0xFA, 0xFF, 0xF0, 0xFE, 0xFD, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFA, 0xFF, + 0xFE, 0xFF, 0xFC, 0xFE, 0xFB, 0xFF, 0xFD, 0xFA, 0xFF, 0xFF, 0x1D, 0xAF, 0xE7, 0xFF, 0x2A, 0xB0, 0xEE, 0xFF, 0x37, + 0xB1, 0xF5, 0xFF, 0x24, 0xB8, 0xF6, 0xFF, 0x28, 0xB4, 0xF7, 0xFF, 0x21, 0xAF, 0xF4, 0xFF, 0x1A, 0xAA, 0xF2, 0xFF, + 0x9E, 0xD7, 0xF5, 0xFF, 0xFC, 0xFF, 0xE9, 0xFF, 0xFC, 0xFE, 0xE0, 0xFF, 0xFC, 0xFD, 0xD7, 0xFF, 0xF8, 0xFA, 0xDA, + 0xFF, 0xF3, 0xF7, 0xDD, 0xFF, 0xFD, 0xFD, 0xF4, 0xFF, 0xF6, 0xF9, 0xE0, 0xFF, 0xDF, 0xEC, 0xC3, 0xFF, 0xD7, 0xEF, + 0xB5, 0xFF, 0xD2, 0xEC, 0xA5, 0xFF, 0xCC, 0xE9, 0x95, 0xFF, 0xBB, 0xE5, 0x67, 0xFF, 0xAB, 0xDB, 0x55, 0xFF, 0x9E, + 0xD3, 0x44, 0xFF, 0x91, 0xCB, 0x32, 0xFF, 0x85, 0xC8, 0x24, 0xFF, 0x79, 0xB4, 0x6A, 0xFF, 0x3A, 0x9D, 0xAF, 0xFF, + 0x0B, 0x97, 0xFF, 0xFF, 0x18, 0x93, 0xF9, 0xFF, 0x0F, 0x9B, 0xED, 0xFF, 0x12, 0x9A, 0xF0, 0xFF, 0x15, 0x98, 0xF3, + 0xFF, 0x13, 0x96, 0xF1, 0xFF, 0x11, 0x94, 0xEF, 0xFF, 0x11, 0x94, 0xEF, 0xFF, 0x58, 0xCF, 0xF4, 0xFF, 0x55, 0xCE, + 0xF4, 0xFF, 0x53, 0xCD, 0xF4, 0xFF, 0x52, 0xCC, 0xF6, 0xFF, 0x52, 0xCB, 0xF8, 0xFF, 0x52, 0xD5, 0xFA, 0xFF, 0x4E, + 0xC7, 0xFB, 0xFF, 0x00, 0x4C, 0xAD, 0xFF, 0x09, 0x6F, 0xCA, 0xFF, 0x0B, 0x7F, 0xD3, 0xFF, 0x05, 0x88, 0xD4, 0xFF, + 0x04, 0x97, 0xDB, 0xFF, 0x04, 0xA7, 0xE1, 0xFF, 0x18, 0xB6, 0xE5, 0xFF, 0x3F, 0xC7, 0xF1, 0xFF, 0x62, 0xD3, 0xF3, + 0xFF, 0x86, 0xDF, 0xF4, 0xFF, 0x91, 0xE4, 0xF7, 0xFF, 0x9B, 0xE9, 0xF9, 0xFF, 0xAD, 0xF0, 0xF9, 0xFF, 0xBF, 0xF7, + 0xF9, 0xFF, 0xCB, 0xFA, 0xFB, 0xFF, 0xD7, 0xFC, 0xFD, 0xFF, 0xDE, 0xFD, 0xFC, 0xFF, 0xE5, 0xFD, 0xFB, 0xFF, 0xEF, + 0xFF, 0xFE, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xF2, 0xFE, 0xFA, 0xFF, 0xFE, 0xFE, 0xFC, 0xFF, 0xC6, 0xE9, 0xFB, 0xFF, + 0x1D, 0xAF, 0xEC, 0xFF, 0x30, 0xB4, 0xF6, 0xFF, 0x2F, 0xB6, 0xF8, 0xFF, 0x19, 0xA7, 0xF6, 0xFF, 0x26, 0xB0, 0xF0, + 0xFF, 0x22, 0xAD, 0xF2, 0xFF, 0x1D, 0xAB, 0xF5, 0xFF, 0x26, 0xA9, 0xF9, 0xFF, 0x1C, 0xA6, 0xF6, 0xFF, 0x7D, 0xCD, + 0xE9, 0xFF, 0xDF, 0xF4, 0xDC, 0xFF, 0xEA, 0xFE, 0xAF, 0xFF, 0xFD, 0xFD, 0xED, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFB, + 0xF8, 0xD3, 0xFF, 0xEC, 0xEE, 0xB4, 0xFF, 0xE6, 0xE9, 0xAB, 0xFF, 0xD8, 0xE6, 0x89, 0xFF, 0xCB, 0xE2, 0x67, 0xFF, + 0xB8, 0xE1, 0x52, 0xFF, 0xA6, 0xDD, 0x4C, 0xFF, 0x74, 0xC5, 0x7E, 0xFF, 0x42, 0xAD, 0xB0, 0xFF, 0x22, 0x9B, 0xF3, + 0xFF, 0x09, 0x9C, 0xFF, 0xFF, 0x09, 0x98, 0xF5, 0xFF, 0x10, 0x9C, 0xEE, 0xFF, 0x17, 0x99, 0xED, 0xFF, 0x14, 0x9D, + 0xED, 0xFF, 0x14, 0x9B, 0xEF, 0xFF, 0x15, 0x99, 0xF2, 0xFF, 0x13, 0x97, 0xF0, 0xFF, 0x11, 0x95, 0xEE, 0xFF, 0x11, + 0x95, 0xEE, 0xFF, 0x5A, 0xD0, 0xF5, 0xFF, 0x57, 0xCF, 0xF4, 0xFF, 0x54, 0xCE, 0xF3, 0xFF, 0x53, 0xCC, 0xF5, 0xFF, + 0x53, 0xCB, 0xF7, 0xFF, 0x4C, 0xD3, 0xF4, 0xFF, 0x2C, 0x9A, 0xDD, 0xFF, 0x03, 0x5D, 0xC1, 0xFF, 0x05, 0x72, 0xC8, + 0xFF, 0x06, 0x83, 0xD2, 0xFF, 0x07, 0x93, 0xDC, 0xFF, 0x07, 0xA2, 0xE1, 0xFF, 0x08, 0xB0, 0xE7, 0xFF, 0x27, 0xBF, + 0xEE, 0xFF, 0x47, 0xCD, 0xF6, 0xFF, 0x6B, 0xD8, 0xF7, 0xFF, 0x8E, 0xE2, 0xF9, 0xFF, 0x9A, 0xE7, 0xFA, 0xFF, 0xA6, + 0xEC, 0xFB, 0xFF, 0xB6, 0xF3, 0xFA, 0xFF, 0xC7, 0xF9, 0xFA, 0xFF, 0xD0, 0xFB, 0xFB, 0xFF, 0xD9, 0xFC, 0xFD, 0xFF, + 0xDD, 0xFD, 0xFC, 0xFF, 0xE2, 0xFE, 0xFC, 0xFF, 0xEE, 0xFF, 0xFE, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xEA, 0xFD, 0xF7, + 0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0x8F, 0xD7, 0xF7, 0xFF, 0x1E, 0xAF, 0xF1, 0xFF, 0x2E, 0xB0, 0xF6, 0xFF, 0x17, 0xAB, + 0xEB, 0xFF, 0xDF, 0xF7, 0xFD, 0xFF, 0x24, 0xAC, 0xE9, 0xFF, 0x22, 0xAC, 0xF0, 0xFF, 0x21, 0xAC, 0xF8, 0xFF, 0x26, + 0xAE, 0xF6, 0xFF, 0x2B, 0xB0, 0xF5, 0xFF, 0x19, 0xA9, 0xF4, 0xFF, 0x08, 0xA2, 0xF3, 0xFF, 0x22, 0xA7, 0xF9, 0xFF, + 0x4C, 0xC1, 0xF2, 0xFF, 0x6D, 0xCD, 0xEE, 0xFF, 0x7D, 0xC9, 0xDB, 0xFF, 0x7F, 0xCA, 0xC2, 0xFF, 0x81, 0xC5, 0xC6, + 0xFF, 0x60, 0xBC, 0xCB, 0xFF, 0x40, 0xB3, 0xCF, 0xFF, 0x24, 0xA7, 0xE9, 0xFF, 0x07, 0x9B, 0xFF, 0xFF, 0x10, 0x9D, + 0xFF, 0xFF, 0x1A, 0x9F, 0xFF, 0xFF, 0x0F, 0x98, 0xE9, 0xFF, 0x14, 0x9C, 0xF9, 0xFF, 0x14, 0x9C, 0xF7, 0xFF, 0x14, + 0x9B, 0xF4, 0xFF, 0x17, 0x9D, 0xF1, 0xFF, 0x19, 0x9E, 0xED, 0xFF, 0x16, 0x9C, 0xEF, 0xFF, 0x14, 0x99, 0xF1, 0xFF, + 0x12, 0x97, 0xEF, 0xFF, 0x10, 0x95, 0xED, 0xFF, 0x10, 0x95, 0xED, 0xFF, 0x5C, 0xD1, 0xF6, 0xFF, 0x58, 0xD0, 0xF4, + 0xFF, 0x55, 0xCF, 0xF3, 0xFF, 0x54, 0xCD, 0xF5, 0xFF, 0x53, 0xCC, 0xF7, 0xFF, 0x51, 0xD5, 0xF6, 0xFF, 0x16, 0x7B, + 0xCE, 0xFF, 0x03, 0x67, 0xC6, 0xFF, 0x06, 0x7B, 0xCF, 0xFF, 0x05, 0x8B, 0xD7, 0xFF, 0x05, 0x9B, 0xDF, 0xFF, 0x07, + 0xA8, 0xE4, 0xFF, 0x09, 0xB6, 0xEA, 0xFF, 0x2A, 0xC3, 0xF1, 0xFF, 0x4C, 0xD1, 0xF7, 0xFF, 0x6C, 0xDB, 0xF8, 0xFF, + 0x8D, 0xE4, 0xFA, 0xFF, 0x9C, 0xEA, 0xFA, 0xFF, 0xAB, 0xEF, 0xFB, 0xFF, 0xBC, 0xF5, 0xFA, 0xFF, 0xCD, 0xFA, 0xFA, + 0xFF, 0xD4, 0xFB, 0xFB, 0xFF, 0xDB, 0xFC, 0xFD, 0xFF, 0xDC, 0xFD, 0xFC, 0xFF, 0xDD, 0xFE, 0xFC, 0xFF, 0xE3, 0xFE, + 0xFC, 0xFF, 0xEA, 0xFE, 0xFD, 0xFF, 0xFE, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0xC0, 0xDE, 0xFF, 0x26, + 0xB4, 0xF5, 0xFF, 0x1E, 0xB0, 0xF8, 0xFF, 0x4D, 0xC6, 0xFF, 0xFF, 0xFF, 0xF8, 0xEF, 0xFF, 0xFE, 0xFF, 0xFA, 0xFF, + 0x8B, 0xD8, 0xF6, 0xFF, 0x18, 0xA7, 0xF3, 0xFF, 0x1D, 0xA9, 0xF4, 0xFF, 0x22, 0xAC, 0xF5, 0xFF, 0x22, 0xAB, 0xF2, + 0xFF, 0x22, 0xAB, 0xF0, 0xFF, 0x1A, 0xA3, 0xF2, 0xFF, 0x1A, 0xA6, 0xEE, 0xFF, 0x17, 0xA8, 0xF4, 0xFF, 0x0D, 0xA2, + 0xF3, 0xFF, 0x10, 0xA4, 0xF2, 0xFF, 0x14, 0xA3, 0xFF, 0xFF, 0x15, 0xA3, 0xFC, 0xFF, 0x16, 0xA2, 0xF9, 0xFF, 0x17, + 0xA2, 0xF2, 0xFF, 0x18, 0xA1, 0xEC, 0xFF, 0x0D, 0x99, 0xFD, 0xFF, 0x16, 0x9A, 0xED, 0xFF, 0x00, 0xA0, 0xFF, 0xFF, + 0x2B, 0x9C, 0xE8, 0xFF, 0x60, 0xB5, 0xAF, 0xFF, 0x10, 0x99, 0xF7, 0xFF, 0x14, 0x9B, 0xF2, 0xFF, 0x18, 0x9D, 0xED, + 0xFF, 0x16, 0x9B, 0xEE, 0xFF, 0x13, 0x99, 0xEF, 0xFF, 0x11, 0x97, 0xED, 0xFF, 0x0F, 0x95, 0xEB, 0xFF, 0x0F, 0x95, + 0xEB, 0xFF, 0x5E, 0xD2, 0xF7, 0xFF, 0x5A, 0xD1, 0xF4, 0xFF, 0x56, 0xD0, 0xF2, 0xFF, 0x54, 0xCE, 0xF5, 0xFF, 0x53, + 0xCC, 0xF7, 0xFF, 0x56, 0xD7, 0xF7, 0xFF, 0x00, 0x5B, 0xC0, 0xFF, 0x03, 0x70, 0xCB, 0xFF, 0x06, 0x84, 0xD6, 0xFF, + 0x05, 0x94, 0xDC, 0xFF, 0x03, 0xA3, 0xE2, 0xFF, 0x07, 0xAF, 0xE8, 0xFF, 0x0B, 0xBB, 0xEE, 0xFF, 0x2D, 0xC8, 0xF3, + 0xFF, 0x50, 0xD5, 0xF8, 0xFF, 0x6E, 0xDE, 0xF9, 0xFF, 0x8C, 0xE6, 0xFA, 0xFF, 0x9F, 0xEC, 0xFB, 0xFF, 0xB1, 0xF2, + 0xFB, 0xFF, 0xC2, 0xF7, 0xFA, 0xFF, 0xD3, 0xFB, 0xF9, 0xFF, 0xD8, 0xFC, 0xFB, 0xFF, 0xDD, 0xFC, 0xFD, 0xFF, 0xDB, + 0xFD, 0xFC, 0xFF, 0xD8, 0xFE, 0xFC, 0xFF, 0xD8, 0xFD, 0xFB, 0xFF, 0xD9, 0xFC, 0xFA, 0xFF, 0xE4, 0xFA, 0xFA, 0xFF, + 0xA3, 0xE9, 0xF6, 0xFF, 0x2A, 0xAC, 0xFB, 0xFF, 0x2E, 0xB9, 0xFA, 0xFF, 0x1A, 0xAD, 0xED, 0xFF, 0x99, 0xDA, 0xF7, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xFC, 0xFF, 0xFE, 0xFE, 0xFC, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0x8C, 0xD4, + 0xF9, 0xFF, 0x19, 0xA8, 0xF5, 0xFF, 0x17, 0xA9, 0xF7, 0xFF, 0x16, 0xA9, 0xF8, 0xFF, 0x1A, 0xA7, 0xF3, 0xFF, 0x1E, + 0xA5, 0xED, 0xFF, 0x1F, 0xA7, 0xF1, 0xFF, 0x20, 0xA9, 0xF6, 0xFF, 0x1D, 0xA7, 0xF6, 0xFF, 0x1A, 0xA5, 0xF6, 0xFF, + 0x16, 0xA3, 0xF8, 0xFF, 0x12, 0xA1, 0xFA, 0xFF, 0x0A, 0x9D, 0xFC, 0xFF, 0x03, 0x98, 0xFE, 0xFF, 0x25, 0xA1, 0xF9, + 0xFF, 0x6F, 0xC0, 0xB0, 0xFF, 0xCF, 0xC9, 0x5D, 0xFF, 0xFF, 0xE5, 0x27, 0xFF, 0x73, 0xB4, 0xB3, 0xFF, 0x0B, 0x97, + 0xF9, 0xFF, 0x11, 0x9A, 0xF3, 0xFF, 0x17, 0x9D, 0xED, 0xFF, 0x15, 0x9B, 0xEE, 0xFF, 0x13, 0x9A, 0xEE, 0xFF, 0x11, + 0x98, 0xEC, 0xFF, 0x0F, 0x96, 0xEA, 0xFF, 0x0F, 0x96, 0xEA, 0xFF, 0x5D, 0xD1, 0xF6, 0xFF, 0x5A, 0xD1, 0xF5, 0xFF, + 0x58, 0xD2, 0xF4, 0xFF, 0x53, 0xCE, 0xF3, 0xFF, 0x56, 0xD1, 0xFA, 0xFF, 0x3F, 0xB1, 0xE6, 0xFF, 0x01, 0x64, 0xC6, + 0xFF, 0x02, 0x75, 0xCE, 0xFF, 0x04, 0x87, 0xD7, 0xFF, 0x02, 0x95, 0xDD, 0xFF, 0x00, 0xA4, 0xE4, 0xFF, 0x03, 0xB0, + 0xEA, 0xFF, 0x06, 0xBD, 0xF1, 0xFF, 0x1B, 0xC8, 0xF2, 0xFF, 0x42, 0xD5, 0xFB, 0xFF, 0x63, 0xDD, 0xFB, 0xFF, 0x84, + 0xE5, 0xFB, 0xFF, 0x98, 0xEB, 0xFC, 0xFF, 0xAB, 0xF1, 0xFC, 0xFF, 0xBD, 0xF8, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, + 0xCF, 0xFC, 0xFF, 0xFF, 0xCF, 0xF9, 0xFB, 0xFF, 0xD2, 0xFE, 0xFD, 0xFF, 0xD4, 0xFF, 0xFF, 0xFF, 0xC6, 0xF9, 0xFF, + 0xFF, 0xB7, 0xEE, 0xFF, 0xFF, 0x59, 0xD7, 0xD9, 0xFF, 0x40, 0xB9, 0xE9, 0xFF, 0x2E, 0xB9, 0xFF, 0xFF, 0x2B, 0xB1, + 0xEF, 0xFF, 0x27, 0xAF, 0xEB, 0xFF, 0xDD, 0xEF, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, + 0xFE, 0xFD, 0xFF, 0xFF, 0xFD, 0xF9, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xC1, 0xE8, 0xF0, 0xFF, + 0x83, 0xCD, 0xE6, 0xFF, 0x52, 0xBB, 0xE8, 0xFF, 0x21, 0xA9, 0xEB, 0xFF, 0x13, 0xA1, 0xFF, 0xFF, 0x06, 0x9F, 0xF7, + 0xFF, 0x0F, 0x9F, 0xF8, 0xFF, 0x18, 0xA3, 0xEA, 0xFF, 0x43, 0xB1, 0xE0, 0xFF, 0x6D, 0xC2, 0xC9, 0xFF, 0xAF, 0xD6, + 0x99, 0xFF, 0xF1, 0xEB, 0x6A, 0xFF, 0xEB, 0xEE, 0x31, 0xFF, 0xF8, 0xE6, 0x47, 0xFF, 0xFF, 0xE1, 0x3A, 0xFF, 0xFC, + 0xE1, 0x41, 0xFF, 0x00, 0x98, 0xF4, 0xFF, 0x19, 0xA1, 0xFC, 0xFF, 0x15, 0x9E, 0xF6, 0xFF, 0x11, 0x9A, 0xF1, 0xFF, + 0x13, 0x9A, 0xF0, 0xFF, 0x14, 0x99, 0xF0, 0xFF, 0x12, 0x97, 0xEE, 0xFF, 0x10, 0x95, 0xEC, 0xFF, 0x10, 0x95, 0xEC, + 0xFF, 0x5C, 0xCF, 0xF5, 0xFF, 0x5A, 0xD1, 0xF6, 0xFF, 0x59, 0xD4, 0xF6, 0xFF, 0x51, 0xCD, 0xF2, 0xFF, 0x59, 0xD6, + 0xFE, 0xFF, 0x29, 0x8B, 0xD5, 0xFF, 0x02, 0x6C, 0xCB, 0xFF, 0x01, 0x7A, 0xD1, 0xFF, 0x01, 0x89, 0xD8, 0xFF, 0x00, + 0x97, 0xDE, 0xFF, 0x00, 0xA5, 0xE5, 0xFF, 0x00, 0xB2, 0xEC, 0xFF, 0x02, 0xBE, 0xF3, 0xFF, 0x08, 0xC7, 0xF1, 0xFF, + 0x35, 0xD5, 0xFE, 0xFF, 0x58, 0xDD, 0xFD, 0xFF, 0x7C, 0xE4, 0xFB, 0xFF, 0x91, 0xEA, 0xFC, 0xFF, 0xA6, 0xEF, 0xFE, + 0xFF, 0xB0, 0xF2, 0xFE, 0xFF, 0xBA, 0xF5, 0xFE, 0xFF, 0xBD, 0xF5, 0xFC, 0xFF, 0xC0, 0xF5, 0xF9, 0xFF, 0xC0, 0xF7, + 0xF6, 0xFF, 0xC1, 0xF9, 0xF4, 0xFF, 0xC6, 0xFC, 0xFC, 0xFF, 0xCC, 0xFF, 0xFF, 0xFF, 0xC1, 0xF8, 0xF7, 0xFF, 0x59, + 0xCC, 0xF3, 0xFF, 0x38, 0xB0, 0xF2, 0xFF, 0x37, 0xBA, 0xF5, 0xFF, 0x29, 0xB4, 0xF7, 0xFF, 0xFC, 0xFB, 0xF8, 0xFF, + 0xFC, 0xFD, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFC, 0xF6, 0xFF, 0xFC, 0xFE, 0xF2, + 0xFF, 0xF6, 0xFF, 0xEE, 0xFF, 0xFC, 0xFF, 0xE9, 0xFF, 0xFF, 0xFF, 0xE4, 0xFF, 0xFF, 0xFF, 0xD7, 0xFF, 0xFF, 0xFF, + 0xCA, 0xFF, 0xFF, 0xFB, 0xF1, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xFC, 0xFD, 0xC1, 0xFF, 0xF6, 0xFF, 0x88, 0xFF, 0xFB, + 0xFD, 0x91, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFC, 0xFC, 0x6C, 0xFF, 0xF9, 0xF6, 0x59, 0xFF, 0xF8, 0xEF, 0x58, 0xFF, + 0xF7, 0xE9, 0x57, 0xFF, 0xF6, 0xE3, 0x59, 0xFF, 0xD0, 0xD2, 0x67, 0xFF, 0x08, 0x98, 0xFF, 0xFF, 0x17, 0x9A, 0xEF, + 0xFF, 0x12, 0x99, 0xF1, 0xFF, 0x0C, 0x98, 0xF4, 0xFF, 0x10, 0x99, 0xF3, 0xFF, 0x15, 0x99, 0xF1, 0xFF, 0x13, 0x97, + 0xEF, 0xFF, 0x11, 0x95, 0xED, 0xFF, 0x11, 0x95, 0xED, 0xFF, 0x5E, 0xD1, 0xF9, 0xFF, 0x5B, 0xD3, 0xF7, 0xFF, 0x59, + 0xD4, 0xF6, 0xFF, 0x57, 0xD3, 0xF8, 0xFF, 0x5E, 0xDA, 0xFF, 0xFF, 0x19, 0x70, 0xCD, 0xFF, 0x02, 0x6D, 0xCC, 0xFF, + 0x03, 0x7B, 0xD2, 0xFF, 0x04, 0x88, 0xD9, 0xFF, 0x04, 0x96, 0xDF, 0xFF, 0x04, 0xA5, 0xE6, 0xFF, 0x01, 0xAD, 0xE6, + 0xFF, 0x00, 0xB4, 0xE7, 0xFF, 0x06, 0xBE, 0xEA, 0xFF, 0x23, 0xCA, 0xF5, 0xFF, 0x4B, 0xD7, 0xF8, 0xFF, 0x74, 0xE3, + 0xFB, 0xFF, 0x89, 0xE8, 0xFC, 0xFF, 0x9E, 0xEC, 0xFE, 0xFF, 0xA5, 0xED, 0xFE, 0xFF, 0xAB, 0xEE, 0xFE, 0xFF, 0xAD, + 0xEF, 0xFB, 0xFF, 0xB0, 0xEF, 0xF9, 0xFF, 0xB3, 0xF2, 0xF8, 0xFF, 0xB6, 0xF5, 0xF8, 0xFF, 0xB5, 0xF8, 0xFC, 0xFF, + 0xB5, 0xFB, 0xFF, 0xFF, 0xD9, 0xF3, 0xFF, 0xFF, 0x1A, 0xB9, 0xF1, 0xFF, 0x28, 0xB3, 0xF3, 0xFF, 0x2A, 0xB3, 0xF6, + 0xFF, 0x73, 0xCE, 0xF3, 0xFF, 0xFD, 0xFD, 0xF5, 0xFF, 0xFD, 0xFE, 0xF9, 0xFF, 0xFD, 0xFF, 0xFE, 0xFF, 0xFF, 0xFE, + 0xF8, 0xFF, 0xFF, 0xFD, 0xF3, 0xFF, 0xFD, 0xFE, 0xEE, 0xFF, 0xFA, 0xFE, 0xE9, 0xFF, 0xFC, 0xFF, 0xE3, 0xFF, 0xFF, + 0xFF, 0xDE, 0xFF, 0xFF, 0xFF, 0xD0, 0xFF, 0xFF, 0xFF, 0xC2, 0xFF, 0xFC, 0xFA, 0xD6, 0xFF, 0xFF, 0xFC, 0xF3, 0xFF, + 0xFE, 0xFF, 0xBF, 0xFF, 0xFC, 0xFA, 0xC4, 0xFF, 0xFC, 0xFF, 0x84, 0xFF, 0xFB, 0xFA, 0x8A, 0xFF, 0xFA, 0xF6, 0x79, + 0xFF, 0xF9, 0xF2, 0x68, 0xFF, 0xF6, 0xED, 0x5E, 0xFF, 0xF4, 0xE8, 0x53, 0xFF, 0xF7, 0xE8, 0x48, 0xFF, 0x87, 0xBC, + 0xA8, 0xFF, 0x10, 0x9A, 0xFB, 0xFF, 0x17, 0x9B, 0xF1, 0xFF, 0x14, 0x9A, 0xF1, 0xFF, 0x10, 0x9A, 0xF1, 0xFF, 0x13, + 0x99, 0xF2, 0xFF, 0x16, 0x98, 0xF3, 0xFF, 0x14, 0x96, 0xF1, 0xFF, 0x12, 0x94, 0xEF, 0xFF, 0x12, 0x94, 0xEF, 0xFF, + 0x61, 0xD4, 0xFC, 0xFF, 0x5D, 0xD4, 0xF9, 0xFF, 0x58, 0xD4, 0xF6, 0xFF, 0x55, 0xD1, 0xF5, 0xFF, 0x53, 0xCE, 0xF5, + 0xFF, 0x01, 0x4D, 0xBD, 0xFF, 0x02, 0x6E, 0xCD, 0xFF, 0x04, 0x7B, 0xD3, 0xFF, 0x06, 0x87, 0xDA, 0xFF, 0x09, 0x96, + 0xE0, 0xFF, 0x0C, 0xA5, 0xE6, 0xFF, 0x0A, 0xB0, 0xE9, 0xFF, 0x09, 0xBA, 0xEB, 0xFF, 0x15, 0xC5, 0xF3, 0xFF, 0x21, + 0xD0, 0xFB, 0xFF, 0x46, 0xD9, 0xFB, 0xFF, 0x6B, 0xE3, 0xFB, 0xFF, 0x81, 0xE6, 0xFC, 0xFF, 0x97, 0xE9, 0xFD, 0xFF, + 0x99, 0xE8, 0xFD, 0xFF, 0x9B, 0xE8, 0xFD, 0xFF, 0x9D, 0xE8, 0xFB, 0xFF, 0x9F, 0xE9, 0xF8, 0xFF, 0xA5, 0xEE, 0xFA, + 0xFF, 0xAB, 0xF2, 0xFC, 0xFF, 0xB0, 0xEF, 0xFB, 0xFF, 0xB4, 0xEB, 0xFB, 0xFF, 0x89, 0xDD, 0xF8, 0xFF, 0x27, 0xB4, + 0xF3, 0xFF, 0x3E, 0xBD, 0xF7, 0xFF, 0x1E, 0xAC, 0xF7, 0xFF, 0xBC, 0xE8, 0xF0, 0xFF, 0xFE, 0xFF, 0xF2, 0xFF, 0xFD, + 0xFF, 0xF3, 0xFF, 0xFD, 0xFE, 0xF4, 0xFF, 0xFD, 0xFE, 0xF2, 0xFF, 0xFE, 0xFE, 0xEF, 0xFF, 0xFE, 0xFE, 0xE9, 0xFF, + 0xFE, 0xFE, 0xE3, 0xFF, 0xFD, 0xFD, 0xDD, 0xFF, 0xFD, 0xFD, 0xD7, 0xFF, 0xFC, 0xFE, 0xC8, 0xFF, 0xFB, 0xFF, 0xB9, + 0xFF, 0xF5, 0xFE, 0x9F, 0xFF, 0xFF, 0xFF, 0xCE, 0xFF, 0xFF, 0xF9, 0xF5, 0xFF, 0xFF, 0xFF, 0xC8, 0xFF, 0xFC, 0xF7, + 0xBD, 0xFF, 0xF8, 0xF8, 0x7A, 0xFF, 0xF8, 0xF5, 0x6B, 0xFF, 0xF9, 0xF3, 0x5C, 0xFF, 0xF5, 0xED, 0x55, 0xFF, 0xF1, + 0xE8, 0x4F, 0xFF, 0xF8, 0xEE, 0x37, 0xFF, 0x3E, 0xA6, 0xE9, 0xFF, 0x17, 0x9C, 0xF5, 0xFF, 0x17, 0x9D, 0xF4, 0xFF, + 0x15, 0x9C, 0xF1, 0xFF, 0x14, 0x9B, 0xEE, 0xFF, 0x15, 0x99, 0xF1, 0xFF, 0x17, 0x97, 0xF4, 0xFF, 0x15, 0x95, 0xF2, + 0xFF, 0x13, 0x93, 0xF0, 0xFF, 0x13, 0x93, 0xF0, 0xFF, 0x66, 0xD6, 0xFB, 0xFF, 0x5E, 0xD1, 0xF4, 0xFF, 0x5F, 0xD3, + 0xF6, 0xFF, 0x59, 0xD7, 0xF9, 0xFF, 0x39, 0x9D, 0xDA, 0xFF, 0x08, 0x58, 0xBE, 0xFF, 0x08, 0x6C, 0xCD, 0xFF, 0x0C, + 0x79, 0xD2, 0xFF, 0x0F, 0x87, 0xD7, 0xFF, 0x11, 0x96, 0xDF, 0xFF, 0x13, 0xA5, 0xE7, 0xFF, 0x13, 0xB0, 0xEA, 0xFF, + 0x1B, 0xC2, 0xF5, 0xFF, 0x0F, 0xC8, 0xF3, 0xFF, 0x16, 0xD0, 0xF9, 0xFF, 0x27, 0xD2, 0xF4, 0xFF, 0x4B, 0xD6, 0xF7, + 0xFF, 0x60, 0xDA, 0xF8, 0xFF, 0x76, 0xDE, 0xF9, 0xFF, 0x7F, 0xDF, 0xF9, 0xFF, 0x87, 0xE0, 0xFA, 0xFF, 0x8C, 0xE4, + 0xFB, 0xFF, 0x91, 0xE7, 0xFB, 0xFF, 0x95, 0xEA, 0xFC, 0xFF, 0x9A, 0xED, 0xFC, 0xFF, 0x9E, 0xEA, 0xFB, 0xFF, 0xA3, + 0xE7, 0xFA, 0xFF, 0x5E, 0xCC, 0xFA, 0xFF, 0x2C, 0xB6, 0xF5, 0xFF, 0x24, 0xB8, 0xF9, 0xFF, 0x14, 0xB1, 0xF5, 0xFF, + 0xFF, 0xFB, 0xFF, 0xFF, 0xFD, 0xFF, 0xEC, 0xFF, 0xFF, 0xFF, 0xED, 0xFF, 0xFF, 0xFF, 0xED, 0xFF, 0xFF, 0xFE, 0xEC, + 0xFF, 0xFD, 0xFD, 0xEA, 0xFF, 0xFD, 0xFD, 0xE3, 0xFF, 0xFD, 0xFD, 0xDC, 0xFF, 0xFD, 0xFD, 0xD5, 0xFF, 0xFD, 0xFD, + 0xCE, 0xFF, 0xFC, 0xFC, 0xC1, 0xFF, 0xFB, 0xFB, 0xB4, 0xFF, 0xF5, 0xFA, 0x8D, 0xFF, 0xF8, 0xFC, 0x89, 0xFF, 0xF8, + 0xFA, 0xCB, 0xFF, 0xF7, 0xFE, 0xF1, 0xFF, 0xF9, 0xFF, 0xBD, 0xFF, 0xFA, 0xF9, 0xC2, 0xFF, 0xFB, 0xF8, 0xAC, 0xFF, + 0xFC, 0xF7, 0x96, 0xFF, 0xF9, 0xF4, 0x91, 0xFF, 0xF7, 0xF0, 0x8C, 0xFF, 0xFF, 0xE4, 0xA8, 0xFF, 0x00, 0x95, 0xF6, + 0xFF, 0x07, 0x99, 0xF6, 0xFF, 0x15, 0x9D, 0xF7, 0xFF, 0x15, 0x9D, 0xF3, 0xFF, 0x15, 0x9C, 0xF0, 0xFF, 0x15, 0x9A, + 0xF2, 0xFF, 0x15, 0x98, 0xF4, 0xFF, 0x14, 0x97, 0xF2, 0xFF, 0x12, 0x95, 0xF1, 0xFF, 0x12, 0x95, 0xF1, 0xFF, 0x6A, + 0xD9, 0xFA, 0xFF, 0x60, 0xCE, 0xF0, 0xFF, 0x66, 0xD2, 0xF6, 0xFF, 0x5C, 0xDD, 0xFD, 0xFF, 0x1E, 0x6C, 0xC0, 0xFF, + 0x0E, 0x63, 0xBE, 0xFF, 0x0E, 0x69, 0xCD, 0xFF, 0x13, 0x78, 0xD0, 0xFF, 0x18, 0x87, 0xD4, 0xFF, 0x19, 0x96, 0xDE, + 0xFF, 0x1A, 0xA6, 0xE9, 0xFF, 0x13, 0xA8, 0xE3, 0xFF, 0x1D, 0xBA, 0xEE, 0xFF, 0x0C, 0xBD, 0xEA, 0xFF, 0x22, 0xC5, + 0xF6, 0xFF, 0x13, 0xC5, 0xEC, 0xFF, 0x2A, 0xCA, 0xF2, 0xFF, 0x40, 0xCF, 0xF3, 0xFF, 0x56, 0xD3, 0xF4, 0xFF, 0x65, + 0xD6, 0xF5, 0xFF, 0x74, 0xD9, 0xF7, 0xFF, 0x7B, 0xDF, 0xFA, 0xFF, 0x82, 0xE4, 0xFE, 0xFF, 0x86, 0xE6, 0xFD, 0xFF, + 0x89, 0xE7, 0xFD, 0xFF, 0x8D, 0xE4, 0xFB, 0xFF, 0x92, 0xE2, 0xF9, 0xFF, 0x33, 0xBB, 0xFC, 0xFF, 0x31, 0xB9, 0xF6, + 0xFF, 0x31, 0xBA, 0xFD, 0xFF, 0x57, 0xC4, 0xF7, 0xFF, 0xF3, 0xFF, 0xDE, 0xFF, 0xFD, 0xFF, 0xE6, 0xFF, 0xFF, 0xFF, + 0xE6, 0xFF, 0xFF, 0xFF, 0xE6, 0xFF, 0xFF, 0xFE, 0xE6, 0xFF, 0xFD, 0xFC, 0xE5, 0xFF, 0xFD, 0xFC, 0xDD, 0xFF, 0xFD, + 0xFC, 0xD5, 0xFF, 0xFD, 0xFD, 0xCD, 0xFF, 0xFD, 0xFD, 0xC5, 0xFF, 0xFC, 0xFA, 0xBA, 0xFF, 0xFB, 0xF7, 0xAF, 0xFF, + 0xFE, 0xF9, 0x9E, 0xFF, 0xFF, 0xFB, 0x8D, 0xFF, 0xFA, 0xFE, 0x77, 0xFF, 0xF4, 0xFB, 0x7D, 0xFF, 0xF8, 0xF7, 0xD2, + 0xFF, 0xFD, 0xFF, 0xEE, 0xFF, 0xFE, 0xFD, 0xDF, 0xFF, 0xFE, 0xFB, 0xD0, 0xFF, 0xFD, 0xFA, 0xCD, 0xFF, 0xFC, 0xF9, + 0xC9, 0xFF, 0xA6, 0xD2, 0xCD, 0xFF, 0x02, 0x98, 0xEA, 0xFF, 0x1E, 0xA0, 0xEC, 0xFF, 0x13, 0x9E, 0xF9, 0xFF, 0x15, + 0x9E, 0xF6, 0xFF, 0x16, 0x9D, 0xF2, 0xFF, 0x15, 0x9B, 0xF2, 0xFF, 0x14, 0x99, 0xF3, 0xFF, 0x13, 0x98, 0xF2, 0xFF, + 0x12, 0x97, 0xF1, 0xFF, 0x12, 0x97, 0xF1, 0xFF, 0x55, 0xD4, 0xF3, 0xFF, 0x5B, 0xD1, 0xF0, 0xFF, 0x69, 0xD6, 0xF6, + 0xFF, 0x6D, 0xE2, 0xFF, 0xFF, 0x0B, 0x4F, 0xA7, 0xFF, 0x11, 0x60, 0xBE, 0xFF, 0x0F, 0x6A, 0xCD, 0xFF, 0x1F, 0x83, + 0xD5, 0xFF, 0x1E, 0x89, 0xDC, 0xFF, 0x0F, 0x8B, 0xDD, 0xFF, 0x1A, 0x9B, 0xE0, 0xFF, 0x22, 0xB0, 0xF3, 0xFF, 0x1D, + 0xAA, 0xE0, 0xFF, 0x13, 0xAE, 0xDF, 0xFF, 0x25, 0xBC, 0xEE, 0xFF, 0x14, 0xB9, 0xE6, 0xFF, 0x1F, 0xC1, 0xEF, 0xFF, + 0x25, 0xC7, 0xEF, 0xFF, 0x2B, 0xCD, 0xEE, 0xFF, 0x3C, 0xCD, 0xF0, 0xFF, 0x4E, 0xCE, 0xF3, 0xFF, 0x5B, 0xD6, 0xF8, + 0xFF, 0x68, 0xDE, 0xFE, 0xFF, 0x6D, 0xDD, 0xFC, 0xFF, 0x73, 0xDC, 0xFA, 0xFF, 0x75, 0xDD, 0xF5, 0xFF, 0x70, 0xD3, + 0xF6, 0xFF, 0x30, 0xBA, 0xFB, 0xFF, 0x33, 0xB8, 0xF5, 0xFF, 0x24, 0xB5, 0xFE, 0xFF, 0xA3, 0xDE, 0xE4, 0xFF, 0xF9, + 0xFF, 0xDC, 0xFF, 0xFD, 0xFD, 0xDC, 0xFF, 0xFE, 0xFE, 0xDC, 0xFF, 0xFF, 0xFF, 0xDB, 0xFF, 0xFE, 0xFE, 0xDA, 0xFF, + 0xFC, 0xFD, 0xD9, 0xFF, 0xFC, 0xFD, 0xD2, 0xFF, 0xFC, 0xFD, 0xCA, 0xFF, 0xFD, 0xFD, 0xC3, 0xFF, 0xFD, 0xFD, 0xBB, + 0xFF, 0xFC, 0xFB, 0xAF, 0xFF, 0xFC, 0xFA, 0xA2, 0xFF, 0xFD, 0xFA, 0x92, 0xFF, 0xFE, 0xFB, 0x83, 0xFF, 0xFB, 0xFD, + 0x6A, 0xFF, 0xF8, 0xFC, 0x60, 0xFF, 0xFA, 0xF8, 0x5D, 0xFF, 0xFC, 0xF7, 0x4C, 0xFF, 0xFD, 0xF4, 0x76, 0xFF, 0xFE, + 0xF2, 0xA0, 0xFF, 0xF5, 0xEC, 0x87, 0xFF, 0xF7, 0xE3, 0x5F, 0xFF, 0x50, 0xBB, 0xB4, 0xFF, 0x0C, 0x99, 0xFE, 0xFF, + 0x1A, 0x9E, 0xF7, 0xFF, 0x15, 0x9D, 0xF6, 0xFF, 0x15, 0x9D, 0xF4, 0xFF, 0x15, 0x9C, 0xF2, 0xFF, 0x14, 0x9B, 0xF2, + 0xFF, 0x12, 0x99, 0xF1, 0xFF, 0x12, 0x99, 0xF1, 0xFF, 0x12, 0x99, 0xF1, 0xFF, 0x12, 0x99, 0xF1, 0xFF, 0x66, 0xD3, + 0xFD, 0xFF, 0x69, 0xD6, 0xF9, 0xFF, 0x6B, 0xD9, 0xF5, 0xFF, 0x4E, 0xB6, 0xDC, 0xFF, 0x18, 0x52, 0xAE, 0xFF, 0x1C, + 0x66, 0xC6, 0xFF, 0x00, 0x5A, 0xBD, 0xFF, 0x1A, 0x7D, 0xCA, 0xFF, 0x15, 0x7B, 0xD4, 0xFF, 0x04, 0x81, 0xDC, 0xFF, + 0x2A, 0xA0, 0xE7, 0xFF, 0x00, 0x88, 0xD3, 0xFF, 0x2D, 0xAB, 0xE2, 0xFF, 0x23, 0xA7, 0xDC, 0xFF, 0x29, 0xB3, 0xE6, + 0xFF, 0x16, 0xAD, 0xE0, 0xFF, 0x14, 0xB7, 0xEB, 0xFF, 0x15, 0xB9, 0xEA, 0xFF, 0x16, 0xBA, 0xE9, 0xFF, 0x1F, 0xBE, + 0xEC, 0xFF, 0x28, 0xC2, 0xEE, 0xFF, 0x3B, 0xCD, 0xF6, 0xFF, 0x4E, 0xD8, 0xFE, 0xFF, 0x55, 0xD5, 0xFB, 0xFF, 0x5D, + 0xD1, 0xF7, 0xFF, 0x5D, 0xD6, 0xEF, 0xFF, 0x4E, 0xC4, 0xF3, 0xFF, 0x2E, 0xB9, 0xFA, 0xFF, 0x35, 0xB8, 0xF4, 0xFF, + 0x17, 0xB1, 0xFF, 0xFF, 0xF0, 0xF7, 0xD1, 0xFF, 0xFE, 0xFF, 0xDA, 0xFF, 0xFC, 0xFC, 0xD2, 0xFF, 0xFD, 0xFD, 0xD1, + 0xFF, 0xFD, 0xFE, 0xD0, 0xFF, 0xFC, 0xFD, 0xCF, 0xFF, 0xFB, 0xFD, 0xCD, 0xFF, 0xFC, 0xFD, 0xC6, 0xFF, 0xFC, 0xFD, + 0xBF, 0xFF, 0xFD, 0xFD, 0xB9, 0xFF, 0xFD, 0xFC, 0xB2, 0xFF, 0xFD, 0xFC, 0xA3, 0xFF, 0xFD, 0xFC, 0x95, 0xFF, 0xFC, + 0xFC, 0x87, 0xFF, 0xFC, 0xFB, 0x78, 0xFF, 0xFD, 0xFA, 0x6C, 0xFF, 0xFD, 0xF8, 0x5F, 0xFF, 0xF9, 0xF6, 0x45, 0xFF, + 0xF5, 0xEF, 0x47, 0xFF, 0xF2, 0xE9, 0x37, 0xFF, 0xEE, 0xE4, 0x28, 0xFF, 0xED, 0xE3, 0x24, 0xFF, 0xFF, 0xDD, 0x05, + 0xFF, 0x03, 0x99, 0xFF, 0xFF, 0x16, 0xA0, 0xF5, 0xFF, 0x16, 0x9E, 0xF4, 0xFF, 0x16, 0x9C, 0xF3, 0xFF, 0x15, 0x9C, + 0xF2, 0xFF, 0x14, 0x9B, 0xF2, 0xFF, 0x12, 0x9A, 0xF1, 0xFF, 0x10, 0x99, 0xEF, 0xFF, 0x11, 0x9A, 0xF0, 0xFF, 0x12, + 0x9B, 0xF1, 0xFF, 0x12, 0x9B, 0xF1, 0xFF, 0x65, 0xD5, 0xFB, 0xFF, 0x70, 0xD4, 0xFC, 0xFF, 0x77, 0xE2, 0xFF, 0xFF, + 0x3B, 0x86, 0xC7, 0xFF, 0x23, 0x5F, 0xBA, 0xFF, 0x1E, 0x6A, 0xBA, 0xFF, 0x21, 0x7A, 0xD0, 0xFF, 0x27, 0x87, 0xD7, + 0xFF, 0x24, 0x8C, 0xD6, 0xFF, 0x1D, 0x8D, 0xD3, 0xFF, 0x21, 0x88, 0xD0, 0xFF, 0x2B, 0xA0, 0xEA, 0xFF, 0x21, 0x95, + 0xD5, 0xFF, 0x30, 0xA9, 0xEE, 0xFF, 0x20, 0xA0, 0xDA, 0xFF, 0x16, 0xA1, 0xDD, 0xFF, 0x0D, 0xA1, 0xDF, 0xFF, 0x19, + 0xAB, 0xE2, 0xFF, 0x12, 0xB1, 0xEB, 0xFF, 0x0F, 0xB8, 0xED, 0xFF, 0x0C, 0xBF, 0xEE, 0xFF, 0x1C, 0xC1, 0xEF, 0xFF, + 0x2C, 0xC3, 0xF0, 0xFF, 0x36, 0xC4, 0xF1, 0xFF, 0x40, 0xC5, 0xF3, 0xFF, 0x46, 0xC9, 0xF1, 0xFF, 0x45, 0xC2, 0xF6, + 0xFF, 0x31, 0xBA, 0xF9, 0xFF, 0x30, 0xB7, 0xF6, 0xFF, 0x4B, 0xC1, 0xF4, 0xFF, 0xF5, 0xFA, 0xC0, 0xFF, 0xFD, 0xFF, + 0xC6, 0xFF, 0xFC, 0xFC, 0xC4, 0xFF, 0xFD, 0xFC, 0xC4, 0xFF, 0xFD, 0xFD, 0xC3, 0xFF, 0xFC, 0xFD, 0xC2, 0xFF, 0xFB, + 0xFC, 0xC1, 0xFF, 0xF8, 0xF8, 0xB5, 0xFF, 0xFC, 0xFD, 0xB2, 0xFF, 0xFC, 0xFC, 0xAA, 0xFF, 0xFC, 0xFC, 0xA3, 0xFF, + 0xFC, 0xFB, 0x95, 0xFF, 0xFB, 0xFB, 0x88, 0xFF, 0xFB, 0xFB, 0x7A, 0xFF, 0xFB, 0xFA, 0x6D, 0xFF, 0xFB, 0xF8, 0x61, + 0xFF, 0xFC, 0xF6, 0x56, 0xFF, 0xF8, 0xF2, 0x44, 0xFF, 0xF4, 0xEA, 0x40, 0xFF, 0xEF, 0xE5, 0x31, 0xFF, 0xEA, 0xDF, + 0x23, 0xFF, 0xFA, 0xE0, 0x1C, 0xFF, 0xC5, 0xD1, 0x44, 0xFF, 0x0A, 0xA1, 0xFE, 0xFF, 0x15, 0x9F, 0xF9, 0xFF, 0x17, + 0x9F, 0xF5, 0xFF, 0x18, 0x9F, 0xF2, 0xFF, 0x16, 0x9E, 0xF2, 0xFF, 0x15, 0x9D, 0xF2, 0xFF, 0x16, 0x9F, 0xF5, 0xFF, + 0x18, 0xA0, 0xF8, 0xFF, 0x15, 0x9D, 0xF5, 0xFF, 0x12, 0x9A, 0xF2, 0xFF, 0x12, 0x9A, 0xF2, 0xFF, 0x64, 0xD7, 0xF9, + 0xFF, 0x64, 0xD1, 0xF6, 0xFF, 0x5D, 0xE6, 0xFF, 0xFF, 0x03, 0x43, 0x9A, 0xFF, 0x0D, 0x4B, 0xA5, 0xFF, 0x30, 0x7B, + 0xCC, 0xFF, 0x04, 0x54, 0xC0, 0xFF, 0x00, 0x53, 0xC9, 0xFF, 0x03, 0x67, 0xC5, 0xFF, 0x25, 0x87, 0xC9, 0xFF, 0x28, + 0x80, 0xCA, 0xFF, 0x27, 0x88, 0xD0, 0xFF, 0x26, 0x90, 0xD7, 0xFF, 0x06, 0x74, 0xC9, 0xFF, 0x17, 0x8D, 0xCF, 0xFF, + 0x1E, 0x9C, 0xE1, 0xFF, 0x16, 0x9B, 0xE3, 0xFF, 0x1E, 0x9E, 0xDA, 0xFF, 0x00, 0x97, 0xDD, 0xFF, 0x03, 0xA4, 0xE6, + 0xFF, 0x07, 0xB1, 0xEE, 0xFF, 0x08, 0xB0, 0xE8, 0xFF, 0x09, 0xAE, 0xE2, 0xFF, 0x16, 0xB4, 0xE8, 0xFF, 0x23, 0xB9, + 0xEF, 0xFF, 0x30, 0xBD, 0xF4, 0xFF, 0x3C, 0xC1, 0xF9, 0xFF, 0x34, 0xBB, 0xF9, 0xFF, 0x2C, 0xB6, 0xF9, 0xFF, 0x80, + 0xD2, 0xE8, 0xFF, 0xFA, 0xFD, 0xAE, 0xFF, 0xFB, 0xFC, 0xB2, 0xFF, 0xFC, 0xFB, 0xB6, 0xFF, 0xFC, 0xFC, 0xB6, 0xFF, + 0xFD, 0xFC, 0xB6, 0xFF, 0xFC, 0xFC, 0xB5, 0xFF, 0xFB, 0xFC, 0xB4, 0xFF, 0xF3, 0xF4, 0xA4, 0xFF, 0xFC, 0xFC, 0xA5, + 0xFF, 0xFC, 0xFC, 0x9C, 0xFF, 0xFC, 0xFB, 0x93, 0xFF, 0xFB, 0xFB, 0x87, 0xFF, 0xFA, 0xFA, 0x7A, 0xFF, 0xFA, 0xFA, + 0x6D, 0xFF, 0xF9, 0xF9, 0x61, 0xFF, 0xFA, 0xF7, 0x57, 0xFF, 0xFA, 0xF4, 0x4E, 0xFF, 0xF6, 0xED, 0x44, 0xFF, 0xF3, + 0xE6, 0x3A, 0xFF, 0xED, 0xE1, 0x2C, 0xFF, 0xE7, 0xDB, 0x1E, 0xFF, 0xFF, 0xD1, 0x19, 0xFF, 0x77, 0xB0, 0x8F, 0xFF, + 0x09, 0xA0, 0xFD, 0xFF, 0x14, 0x9D, 0xFD, 0xFF, 0x17, 0x9F, 0xF7, 0xFF, 0x1A, 0xA2, 0xF2, 0xFF, 0x18, 0xA0, 0xF2, + 0xFF, 0x16, 0x9E, 0xF2, 0xFF, 0x13, 0x9B, 0xF1, 0xFF, 0x10, 0x98, 0xF0, 0xFF, 0x11, 0x99, 0xF1, 0xFF, 0x12, 0x9A, + 0xF2, 0xFF, 0x12, 0x9A, 0xF2, 0xFF, 0x5F, 0xD4, 0xF7, 0xFF, 0x67, 0xDC, 0xFC, 0xFF, 0x4F, 0xC1, 0xF0, 0xFF, 0x00, + 0x2B, 0x8A, 0xFF, 0x2D, 0x6A, 0xBF, 0xFF, 0x05, 0x47, 0xAC, 0xFF, 0x00, 0x43, 0xB9, 0xFF, 0x35, 0x85, 0xC4, 0xFF, + 0x06, 0x4D, 0xBB, 0xFF, 0x13, 0x61, 0xC3, 0xFF, 0x2C, 0x70, 0xCA, 0xFF, 0x0F, 0x5A, 0xB3, 0xFF, 0x21, 0x74, 0xCC, + 0xFF, 0x11, 0x69, 0xC2, 0xFF, 0x18, 0x78, 0xC2, 0xFF, 0x1C, 0x80, 0xD0, 0xFF, 0x18, 0x7F, 0xD6, 0xFF, 0x1A, 0x86, + 0xD3, 0xFF, 0x10, 0x8F, 0xDD, 0xFF, 0x02, 0x8C, 0xDA, 0xFF, 0x04, 0x99, 0xE6, 0xFF, 0x04, 0x9B, 0xE1, 0xFF, 0x04, + 0x9D, 0xDC, 0xFF, 0x05, 0xA6, 0xE1, 0xFF, 0x00, 0xA6, 0xDD, 0xFF, 0x1F, 0xB6, 0xEE, 0xFF, 0x39, 0xBD, 0xF6, 0xFF, + 0x38, 0xBB, 0xF6, 0xFF, 0x24, 0xB5, 0xFC, 0xFF, 0xBF, 0xE8, 0xB8, 0xFF, 0xFA, 0xFE, 0xA2, 0xFF, 0xFB, 0xFC, 0xA5, + 0xFF, 0xFB, 0xFA, 0xA8, 0xFF, 0xFC, 0xFB, 0xA7, 0xFF, 0xFC, 0xFC, 0xA6, 0xFF, 0xFA, 0xFB, 0xA2, 0xFF, 0xF8, 0xFA, + 0x9F, 0xFF, 0xF5, 0xF7, 0x94, 0xFF, 0xFA, 0xFB, 0x92, 0xFF, 0xFA, 0xFB, 0x8B, 0xFF, 0xFB, 0xFB, 0x84, 0xFF, 0xFA, + 0xFA, 0x78, 0xFF, 0xF9, 0xF9, 0x6D, 0xFF, 0xF9, 0xF9, 0x61, 0xFF, 0xF8, 0xF8, 0x55, 0xFF, 0xF8, 0xF6, 0x4B, 0xFF, + 0xF9, 0xF3, 0x41, 0xFF, 0xF5, 0xEC, 0x39, 0xFF, 0xF1, 0xE4, 0x30, 0xFF, 0xEE, 0xDD, 0x28, 0xFF, 0xEB, 0xD6, 0x1F, + 0xFF, 0xEE, 0xD9, 0x00, 0xFF, 0x32, 0xA6, 0xE4, 0xFF, 0x18, 0xA4, 0xFF, 0xFF, 0x28, 0xA4, 0xF3, 0xFF, 0x20, 0xA2, + 0xF4, 0xFF, 0x18, 0xA0, 0xF4, 0xFF, 0x16, 0x9E, 0xF4, 0xFF, 0x15, 0x9D, 0xF3, 0xFF, 0x13, 0x9B, 0xF2, 0xFF, 0x11, + 0x99, 0xF2, 0xFF, 0x11, 0x99, 0xF2, 0xFF, 0x12, 0x9A, 0xF3, 0xFF, 0x12, 0x9A, 0xF3, 0xFF, 0x5B, 0xD1, 0xF5, 0xFF, + 0x62, 0xDF, 0xFA, 0xFF, 0x30, 0x8C, 0xCC, 0xFF, 0x05, 0x2C, 0x91, 0xFF, 0x0E, 0x49, 0x9A, 0xFF, 0x00, 0x36, 0x9E, + 0xFF, 0x00, 0x38, 0x96, 0xFF, 0x14, 0x5E, 0xB6, 0xFF, 0x53, 0xAA, 0xD9, 0xFF, 0x30, 0xA6, 0xE2, 0xFF, 0x44, 0xBB, + 0xEE, 0xFF, 0x6D, 0xDD, 0xFF, 0xFF, 0x76, 0xDE, 0xF9, 0xFF, 0x6C, 0xD9, 0xF9, 0xFF, 0x63, 0xD4, 0xF8, 0xFF, 0x54, + 0xC4, 0xF3, 0xFF, 0x44, 0xB4, 0xED, 0xFF, 0x23, 0x8E, 0xD5, 0xFF, 0x11, 0x77, 0xCE, 0xFF, 0x00, 0x6C, 0xC6, 0xFF, + 0x02, 0x81, 0xDE, 0xFF, 0x00, 0x87, 0xDA, 0xFF, 0x00, 0x8D, 0xD6, 0xFF, 0x06, 0x9B, 0xE1, 0xFF, 0x00, 0x98, 0xDC, + 0xFF, 0x22, 0xB1, 0xF0, 0xFF, 0x35, 0xB9, 0xF4, 0xFF, 0x3C, 0xBC, 0xF3, 0xFF, 0x1B, 0xB4, 0xFF, 0xFF, 0xFE, 0xFD, + 0x89, 0xFF, 0xFA, 0xFF, 0x95, 0xFF, 0xFA, 0xFC, 0x97, 0xFF, 0xFB, 0xF8, 0x99, 0xFF, 0xFB, 0xFB, 0x97, 0xFF, 0xFC, + 0xFD, 0x95, 0xFF, 0xF9, 0xFB, 0x8F, 0xFF, 0xF6, 0xF9, 0x89, 0xFF, 0xF7, 0xF9, 0x84, 0xFF, 0xF8, 0xF9, 0x7F, 0xFF, + 0xF9, 0xFA, 0x7A, 0xFF, 0xFA, 0xFA, 0x75, 0xFF, 0xF9, 0xF9, 0x6A, 0xFF, 0xF8, 0xF9, 0x5F, 0xFF, 0xF7, 0xF8, 0x54, + 0xFF, 0xF6, 0xF7, 0x49, 0xFF, 0xF7, 0xF5, 0x3F, 0xFF, 0xF7, 0xF2, 0x35, 0xFF, 0xF3, 0xEB, 0x2E, 0xFF, 0xF0, 0xE3, + 0x27, 0xFF, 0xF0, 0xDA, 0x24, 0xFF, 0xF0, 0xD1, 0x21, 0xFF, 0xE8, 0xC9, 0x23, 0xFF, 0x03, 0x9B, 0xFF, 0xFF, 0x20, + 0xA3, 0xF6, 0xFF, 0x16, 0xA1, 0xF6, 0xFF, 0x16, 0x9F, 0xF7, 0xFF, 0x16, 0x9D, 0xF7, 0xFF, 0x15, 0x9C, 0xF6, 0xFF, + 0x14, 0x9B, 0xF5, 0xFF, 0x13, 0x9A, 0xF4, 0xFF, 0x12, 0x99, 0xF3, 0xFF, 0x12, 0x99, 0xF3, 0xFF, 0x12, 0x99, 0xF3, + 0xFF, 0x12, 0x99, 0xF3, 0xFF, 0x5A, 0xE2, 0xFE, 0xFF, 0x64, 0xD7, 0xFF, 0xFF, 0x0C, 0x46, 0x97, 0xFF, 0x00, 0x25, + 0x82, 0xFF, 0x1D, 0x6A, 0xB7, 0xFF, 0x39, 0xA2, 0xDE, 0xFF, 0x5E, 0xE5, 0xFF, 0xFF, 0x51, 0xD8, 0xFD, 0xFF, 0x4C, + 0xD6, 0xF5, 0xFF, 0x48, 0xCC, 0xF4, 0xFF, 0x5E, 0xCF, 0xF6, 0xFF, 0x67, 0xD9, 0xFE, 0xFF, 0x61, 0xD3, 0xF7, 0xFF, + 0x5A, 0xD1, 0xF8, 0xFF, 0x41, 0xCB, 0xFE, 0xFF, 0x53, 0xCE, 0xFE, 0xFF, 0x51, 0xCF, 0xF5, 0xFF, 0x49, 0xCA, 0xF6, + 0xFF, 0x49, 0xCD, 0xFF, 0xFF, 0x3F, 0xB9, 0xFF, 0xFF, 0x0E, 0x7E, 0xDA, 0xFF, 0x00, 0x69, 0xC2, 0xFF, 0x05, 0x84, + 0xDA, 0xFF, 0x01, 0x84, 0xD5, 0xFF, 0x05, 0x8C, 0xD8, 0xFF, 0x37, 0xBE, 0xF8, 0xFF, 0x3A, 0xBE, 0xF6, 0xFF, 0x34, + 0xBD, 0xFF, 0xFF, 0x61, 0xC6, 0xE1, 0xFF, 0xFB, 0xF3, 0x79, 0xFF, 0xF7, 0xFA, 0x82, 0xFF, 0xF9, 0xF9, 0x83, 0xFF, + 0xFA, 0xF7, 0x83, 0xFF, 0xF8, 0xF7, 0x7F, 0xFF, 0xF6, 0xF6, 0x7B, 0xFF, 0xF7, 0xF8, 0x79, 0xFF, 0xF8, 0xFA, 0x77, + 0xFF, 0xF7, 0xF9, 0x71, 0xFF, 0xF7, 0xF8, 0x6C, 0xFF, 0xFB, 0xFC, 0x6B, 0xFF, 0xF8, 0xF8, 0x63, 0xFF, 0xF8, 0xF7, + 0x5A, 0xFF, 0xF7, 0xF7, 0x52, 0xFF, 0xF7, 0xF5, 0x48, 0xFF, 0xF6, 0xF4, 0x3F, 0xFF, 0xF5, 0xF2, 0x37, 0xFF, 0xF4, + 0xEF, 0x2F, 0xFF, 0xF1, 0xE6, 0x27, 0xFF, 0xEE, 0xDD, 0x20, 0xFF, 0xEA, 0xD6, 0x1F, 0xFF, 0xF1, 0xCC, 0x10, 0xFF, + 0x9D, 0xB9, 0x6C, 0xFF, 0x0B, 0x9F, 0xFE, 0xFF, 0x1A, 0xA3, 0xF8, 0xFF, 0x16, 0xA2, 0xF9, 0xFF, 0x16, 0xA0, 0xF8, + 0xFF, 0x16, 0x9E, 0xF7, 0xFF, 0x15, 0x9D, 0xF7, 0xFF, 0x14, 0x9B, 0xF6, 0xFF, 0x14, 0x9A, 0xF5, 0xFF, 0x13, 0x99, + 0xF4, 0xFF, 0x13, 0x99, 0xF4, 0xFF, 0x13, 0x99, 0xF4, 0xFF, 0x13, 0x99, 0xF4, 0xFF, 0x60, 0xD8, 0xF8, 0xFF, 0x5A, + 0xD8, 0xF7, 0xFF, 0x4B, 0xAD, 0xD7, 0xFF, 0x68, 0xDD, 0xFF, 0xFF, 0x55, 0xDC, 0xF7, 0xFF, 0x55, 0xD6, 0xFC, 0xFF, + 0x54, 0xCF, 0xFF, 0xFF, 0x5C, 0xD5, 0xFF, 0xFF, 0x53, 0xCA, 0xF1, 0xFF, 0x4A, 0xCA, 0xF5, 0xFF, 0x42, 0xC9, 0xF9, + 0xFF, 0x47, 0xC9, 0xF7, 0xFF, 0x4B, 0xC8, 0xF5, 0xFF, 0x5C, 0xCF, 0xF0, 0xFF, 0x46, 0xCC, 0xF8, 0xFF, 0x55, 0xCA, + 0xFF, 0xFF, 0x3E, 0xC3, 0xF9, 0xFF, 0x43, 0xC2, 0xFB, 0xFF, 0x48, 0xC1, 0xFC, 0xFF, 0x3E, 0xBE, 0xF3, 0xFF, 0x43, + 0xCB, 0xFA, 0xFF, 0x37, 0xB3, 0xFC, 0xFF, 0x0B, 0x7B, 0xDD, 0xFF, 0x00, 0x6D, 0xC8, 0xFF, 0x0D, 0x7F, 0xD4, 0xFF, + 0x4D, 0xCC, 0xFF, 0xFF, 0x3E, 0xC2, 0xF9, 0xFF, 0x2D, 0xC1, 0xFF, 0xFF, 0xA7, 0xDE, 0xA7, 0xFF, 0xF7, 0xEB, 0x5B, + 0xFF, 0xF4, 0xF5, 0x6F, 0xFF, 0xF7, 0xF5, 0x6E, 0xFF, 0xF9, 0xF6, 0x6D, 0xFF, 0xF5, 0xF3, 0x67, 0xFF, 0xF1, 0xF0, + 0x60, 0xFF, 0xF5, 0xF6, 0x62, 0xFF, 0xFA, 0xFC, 0x65, 0xFF, 0xF8, 0xF9, 0x5E, 0xFF, 0xF5, 0xF6, 0x58, 0xFF, 0xFE, + 0xFE, 0x5D, 0xFF, 0xF6, 0xF6, 0x52, 0xFF, 0xF6, 0xF5, 0x4B, 0xFF, 0xF7, 0xF5, 0x44, 0xFF, 0xF6, 0xF3, 0x3D, 0xFF, + 0xF5, 0xF1, 0x35, 0xFF, 0xF3, 0xEE, 0x2F, 0xFF, 0xF0, 0xEB, 0x28, 0xFF, 0xEE, 0xE1, 0x20, 0xFF, 0xEC, 0xD8, 0x18, + 0xFF, 0xE4, 0xD2, 0x1A, 0xFF, 0xF3, 0xC6, 0x00, 0xFF, 0x51, 0xA8, 0xB4, 0xFF, 0x13, 0xA3, 0xFA, 0xFF, 0x15, 0xA3, + 0xFB, 0xFF, 0x17, 0xA3, 0xFB, 0xFF, 0x16, 0xA0, 0xFA, 0xFF, 0x16, 0x9E, 0xF8, 0xFF, 0x15, 0x9D, 0xF7, 0xFF, 0x15, + 0x9C, 0xF7, 0xFF, 0x14, 0x9A, 0xF6, 0xFF, 0x14, 0x99, 0xF6, 0xFF, 0x14, 0x99, 0xF6, 0xFF, 0x14, 0x99, 0xF6, 0xFF, + 0x14, 0x99, 0xF6, 0xFF, 0x58, 0xCE, 0xF1, 0xFF, 0x59, 0xDC, 0xFD, 0xFF, 0x55, 0xD5, 0xF8, 0xFF, 0x5D, 0xDD, 0xFF, + 0xFF, 0x4D, 0xCE, 0xF3, 0xFF, 0x4C, 0xCB, 0xF3, 0xFF, 0x4C, 0xC8, 0xF3, 0xFF, 0x56, 0xD1, 0xFB, 0xFF, 0x58, 0xD3, + 0xFC, 0xFF, 0x4F, 0xCE, 0xFB, 0xFF, 0x47, 0xC9, 0xFA, 0xFF, 0x48, 0xC8, 0xF9, 0xFF, 0x49, 0xC7, 0xF8, 0xFF, 0x50, + 0xCA, 0xF5, 0xFF, 0x44, 0xC9, 0xF9, 0xFF, 0x4B, 0xC8, 0xFD, 0xFF, 0x3E, 0xC5, 0xF9, 0xFF, 0x40, 0xC3, 0xFA, 0xFF, + 0x43, 0xC2, 0xFA, 0xFF, 0x3A, 0xBD, 0xF3, 0xFF, 0x3A, 0xBF, 0xF3, 0xFF, 0x3E, 0xC7, 0xFC, 0xFF, 0x3A, 0xC6, 0xFC, + 0xFF, 0x24, 0xA1, 0xE2, 0xFF, 0x1F, 0x8C, 0xD9, 0xFF, 0x36, 0xB9, 0xF6, 0xFF, 0x26, 0xBB, 0xFA, 0xFF, 0x29, 0xBA, + 0xF3, 0xFF, 0xCD, 0xD7, 0x56, 0xFF, 0xF9, 0xFA, 0x5A, 0xFF, 0xD9, 0xDA, 0x48, 0xFF, 0xED, 0xEC, 0x58, 0xFF, 0xF9, + 0xF5, 0x5F, 0xFF, 0xF1, 0xEF, 0x4D, 0xFF, 0xE9, 0xE9, 0x3A, 0xFF, 0xED, 0xEE, 0x45, 0xFF, 0xF2, 0xF4, 0x50, 0xFF, + 0xF9, 0xF3, 0x4E, 0xFF, 0xED, 0xF0, 0x44, 0xFF, 0xFE, 0xF8, 0x4B, 0xFF, 0xF4, 0xF5, 0x41, 0xFF, 0xF5, 0xF4, 0x3C, + 0xFF, 0xF6, 0xF2, 0x37, 0xFF, 0xF5, 0xF0, 0x31, 0xFF, 0xF4, 0xEF, 0x2A, 0xFF, 0xF2, 0xEA, 0x26, 0xFF, 0xF0, 0xE6, + 0x22, 0xFF, 0xEE, 0xDB, 0x1C, 0xFF, 0xEC, 0xD0, 0x17, 0xFF, 0xF0, 0xCC, 0x08, 0xFF, 0xF5, 0xC4, 0x08, 0xFF, 0x0E, + 0xAD, 0xFF, 0xFF, 0x16, 0xA1, 0xF9, 0xFF, 0x17, 0xA1, 0xF8, 0xFF, 0x18, 0xA1, 0xF8, 0xFF, 0x17, 0xA0, 0xF8, 0xFF, + 0x16, 0x9E, 0xF8, 0xFF, 0x16, 0x9D, 0xF8, 0xFF, 0x15, 0x9C, 0xF8, 0xFF, 0x15, 0x9A, 0xF7, 0xFF, 0x14, 0x99, 0xF7, + 0xFF, 0x14, 0x99, 0xF7, 0xFF, 0x14, 0x99, 0xF7, 0xFF, 0x14, 0x99, 0xF7, 0xFF, 0x60, 0xD5, 0xFB, 0xFF, 0x5A, 0xD3, + 0xFA, 0xFF, 0x55, 0xD1, 0xFA, 0xFF, 0x55, 0xD0, 0xFC, 0xFF, 0x54, 0xCF, 0xFE, 0xFF, 0x54, 0xD0, 0xFA, 0xFF, 0x53, + 0xD1, 0xF6, 0xFF, 0x50, 0xCE, 0xF6, 0xFF, 0x4E, 0xCB, 0xF7, 0xFF, 0x4C, 0xCA, 0xF9, 0xFF, 0x4B, 0xCA, 0xFA, 0xFF, + 0x49, 0xC8, 0xFB, 0xFF, 0x47, 0xC6, 0xFB, 0xFF, 0x45, 0xC6, 0xFB, 0xFF, 0x43, 0xC6, 0xFA, 0xFF, 0x41, 0xC6, 0xF9, + 0xFF, 0x3F, 0xC6, 0xF8, 0xFF, 0x3E, 0xC4, 0xF8, 0xFF, 0x3E, 0xC3, 0xF9, 0xFF, 0x3F, 0xC3, 0xFB, 0xFF, 0x40, 0xC3, + 0xFD, 0xFF, 0x38, 0xBA, 0xF2, 0xFF, 0x3F, 0xC0, 0xF7, 0xFF, 0x3D, 0xC2, 0xFA, 0xFF, 0x3A, 0xC5, 0xFD, 0xFF, 0x37, + 0xC1, 0xF6, 0xFF, 0x34, 0xBD, 0xEF, 0xFF, 0x2D, 0xBB, 0xEF, 0xFF, 0xDD, 0xD6, 0x21, 0xFF, 0xBF, 0xDC, 0x37, 0xFF, + 0xDD, 0xE0, 0x41, 0xFF, 0xEB, 0xEA, 0x49, 0xFF, 0xEA, 0xE3, 0x41, 0xFF, 0xED, 0xE8, 0x41, 0xFF, 0xF1, 0xED, 0x41, + 0xFF, 0xED, 0xEC, 0x3F, 0xFF, 0xEA, 0xEB, 0x3C, 0xFF, 0xFA, 0xEE, 0x3E, 0xFF, 0xE5, 0xEB, 0x31, 0xFF, 0xFE, 0xF2, + 0x39, 0xFF, 0xF1, 0xF4, 0x31, 0xFF, 0xF3, 0xF2, 0x2D, 0xFF, 0xF5, 0xF0, 0x29, 0xFF, 0xF4, 0xEE, 0x25, 0xFF, 0xF4, + 0xEC, 0x20, 0xFF, 0xF1, 0xE6, 0x1E, 0xFF, 0xEF, 0xE1, 0x1C, 0xFF, 0xED, 0xD5, 0x19, 0xFF, 0xEB, 0xC9, 0x16, 0xFF, + 0xDE, 0xC3, 0x0B, 0xFF, 0xBA, 0xBE, 0x39, 0xFF, 0x07, 0x98, 0xF8, 0xFF, 0x19, 0x9F, 0xF8, 0xFF, 0x19, 0x9F, 0xF6, + 0xFF, 0x19, 0x9F, 0xF5, 0xFF, 0x18, 0x9F, 0xF7, 0xFF, 0x16, 0x9F, 0xF9, 0xFF, 0x16, 0x9D, 0xF9, 0xFF, 0x16, 0x9C, + 0xF9, 0xFF, 0x16, 0x9A, 0xF9, 0xFF, 0x15, 0x99, 0xF8, 0xFF, 0x15, 0x99, 0xF8, 0xFF, 0x15, 0x99, 0xF8, 0xFF, 0x15, + 0x99, 0xF8, 0xFF, 0x5C, 0xD4, 0xF8, 0xFF, 0x58, 0xD4, 0xF8, 0xFF, 0x54, 0xD3, 0xF8, 0xFF, 0x56, 0xD1, 0xF9, 0xFF, + 0x57, 0xD0, 0xFA, 0xFF, 0x55, 0xD0, 0xF8, 0xFF, 0x53, 0xD0, 0xF5, 0xFF, 0x50, 0xCE, 0xF7, 0xFF, 0x4D, 0xCC, 0xF9, + 0xFF, 0x4C, 0xCB, 0xF9, 0xFF, 0x4A, 0xCA, 0xFA, 0xFF, 0x48, 0xC8, 0xFB, 0xFF, 0x46, 0xC7, 0xFB, 0xFF, 0x44, 0xC6, + 0xFA, 0xFF, 0x43, 0xC6, 0xFA, 0xFF, 0x41, 0xC6, 0xF9, 0xFF, 0x3F, 0xC6, 0xF9, 0xFF, 0x3E, 0xC4, 0xF9, 0xFF, 0x3D, + 0xC3, 0xF9, 0xFF, 0x3E, 0xC2, 0xFA, 0xFF, 0x3E, 0xC1, 0xFB, 0xFF, 0x3A, 0xBD, 0xF5, 0xFF, 0x3D, 0xC1, 0xF7, 0xFF, + 0x3A, 0xC0, 0xF8, 0xFF, 0x37, 0xC0, 0xF9, 0xFF, 0x36, 0xBD, 0xFF, 0xFF, 0x35, 0xBB, 0xFF, 0xFF, 0x66, 0xBA, 0x84, + 0xFF, 0xAF, 0xD2, 0x18, 0xFF, 0xB3, 0xD2, 0x19, 0xFF, 0xD2, 0xDA, 0x39, 0xFF, 0xE1, 0xDC, 0x3D, 0xFF, 0xD5, 0xD4, + 0x31, 0xFF, 0xE1, 0xDF, 0x37, 0xFF, 0xEC, 0xE9, 0x3E, 0xFF, 0xE1, 0xE6, 0x35, 0xFF, 0xE9, 0xE5, 0x35, 0xFF, 0xF0, + 0xE5, 0x34, 0xFF, 0xE4, 0xE3, 0x2A, 0xFF, 0xF5, 0xE5, 0x2D, 0xFF, 0xE8, 0xEB, 0x28, 0xFF, 0xF0, 0xEE, 0x2A, 0xFF, + 0xEF, 0xE8, 0x24, 0xFF, 0xEC, 0xE4, 0x20, 0xFF, 0xE9, 0xDF, 0x1C, 0xFF, 0xEB, 0xDB, 0x1C, 0xFF, 0xED, 0xD7, 0x1B, + 0xFF, 0xE9, 0xCE, 0x18, 0xFF, 0xE5, 0xC5, 0x15, 0xFF, 0xE7, 0xBF, 0x03, 0xFF, 0x6C, 0xB1, 0x92, 0xFF, 0x10, 0x9C, + 0xFB, 0xFF, 0x17, 0xA0, 0xF7, 0xFF, 0x19, 0xA0, 0xF5, 0xFF, 0x1B, 0xA0, 0xF3, 0xFF, 0x19, 0x9F, 0xF6, 0xFF, 0x16, + 0x9F, 0xF9, 0xFF, 0x16, 0x9E, 0xF8, 0xFF, 0x15, 0x9C, 0xF8, 0xFF, 0x15, 0x9B, 0xF8, 0xFF, 0x15, 0x99, 0xF8, 0xFF, + 0x14, 0x99, 0xF7, 0xFF, 0x14, 0x98, 0xF7, 0xFF, 0x14, 0x98, 0xF7, 0xFF, 0x57, 0xD3, 0xF6, 0xFF, 0x55, 0xD4, 0xF6, + 0xFF, 0x53, 0xD5, 0xF6, 0xFF, 0x57, 0xD2, 0xF7, 0xFF, 0x5B, 0xD0, 0xF7, 0xFF, 0x57, 0xD0, 0xF6, 0xFF, 0x54, 0xCF, + 0xF5, 0xFF, 0x50, 0xCE, 0xF7, 0xFF, 0x4C, 0xCC, 0xFA, 0xFF, 0x4B, 0xCB, 0xFA, 0xFF, 0x49, 0xCA, 0xFA, 0xFF, 0x47, + 0xC8, 0xFA, 0xFF, 0x46, 0xC7, 0xFB, 0xFF, 0x44, 0xC7, 0xFA, 0xFF, 0x43, 0xC6, 0xFA, 0xFF, 0x41, 0xC6, 0xF9, 0xFF, + 0x3F, 0xC5, 0xF9, 0xFF, 0x3E, 0xC4, 0xF9, 0xFF, 0x3D, 0xC2, 0xF9, 0xFF, 0x3C, 0xC1, 0xF9, 0xFF, 0x3B, 0xC0, 0xF9, + 0xFF, 0x3C, 0xC1, 0xF8, 0xFF, 0x3C, 0xC2, 0xF7, 0xFF, 0x38, 0xBE, 0xF6, 0xFF, 0x34, 0xBB, 0xF5, 0xFF, 0x35, 0xBC, + 0xFD, 0xFF, 0x36, 0xBE, 0xFF, 0xFF, 0x45, 0xBB, 0xFB, 0xFF, 0x82, 0xC9, 0x2B, 0xFF, 0xA0, 0xBE, 0x01, 0xFF, 0xB8, + 0xC4, 0x20, 0xFF, 0xD8, 0xCF, 0x31, 0xFF, 0xD1, 0xD5, 0x31, 0xFF, 0xD4, 0xD5, 0x2E, 0xFF, 0xD7, 0xD4, 0x2A, 0xFF, + 0xCC, 0xD7, 0x24, 0xFF, 0xE8, 0xDE, 0x2E, 0xFF, 0xE6, 0xDD, 0x29, 0xFF, 0xE4, 0xDC, 0x24, 0xFF, 0xED, 0xD9, 0x22, + 0xFF, 0xDF, 0xE1, 0x20, 0xFF, 0xEC, 0xE9, 0x27, 0xFF, 0xEA, 0xE0, 0x1E, 0xFF, 0xE3, 0xD9, 0x1B, 0xFF, 0xDD, 0xD3, + 0x19, 0xFF, 0xE4, 0xD0, 0x1A, 0xFF, 0xEB, 0xCD, 0x1B, 0xFF, 0xE4, 0xC7, 0x17, 0xFF, 0xDE, 0xC2, 0x14, 0xFF, 0xEF, + 0xBC, 0x00, 0xFF, 0x1D, 0xA4, 0xEB, 0xFF, 0x19, 0xA0, 0xFF, 0xFF, 0x15, 0xA2, 0xF6, 0xFF, 0x19, 0xA2, 0xF3, 0xFF, + 0x1D, 0xA1, 0xF0, 0xFF, 0x19, 0xA0, 0xF4, 0xFF, 0x16, 0x9F, 0xF8, 0xFF, 0x15, 0x9E, 0xF8, 0xFF, 0x15, 0x9D, 0xF8, + 0xFF, 0x14, 0x9B, 0xF7, 0xFF, 0x14, 0x9A, 0xF7, 0xFF, 0x13, 0x99, 0xF6, 0xFF, 0x12, 0x98, 0xF5, 0xFF, 0x12, 0x98, + 0xF5, 0xFF, 0x5E, 0xD5, 0xF8, 0xFF, 0x63, 0xD5, 0xFC, 0xFF, 0x68, 0xD6, 0xFF, 0xFF, 0x5E, 0xD2, 0xFB, 0xFF, 0x55, + 0xCF, 0xF8, 0xFF, 0x53, 0xCF, 0xF7, 0xFF, 0x50, 0xCE, 0xF7, 0xFF, 0x4D, 0xCD, 0xF9, 0xFF, 0x4B, 0xCC, 0xFA, 0xFF, + 0x49, 0xCB, 0xFA, 0xFF, 0x48, 0xCA, 0xFA, 0xFF, 0x47, 0xC9, 0xFA, 0xFF, 0x45, 0xC8, 0xFA, 0xFF, 0x44, 0xC7, 0xFA, + 0xFF, 0x42, 0xC6, 0xF9, 0xFF, 0x41, 0xC5, 0xF9, 0xFF, 0x40, 0xC5, 0xF9, 0xFF, 0x3F, 0xC4, 0xF9, 0xFF, 0x3D, 0xC2, + 0xF9, 0xFF, 0x3C, 0xC1, 0xF9, 0xFF, 0x3B, 0xC0, 0xF8, 0xFF, 0x3B, 0xC0, 0xF8, 0xFF, 0x3A, 0xC1, 0xF8, 0xFF, 0x38, + 0xBF, 0xF7, 0xFF, 0x35, 0xBD, 0xF6, 0xFF, 0x34, 0xBD, 0xFA, 0xFF, 0x33, 0xBD, 0xFE, 0xFF, 0x22, 0xC3, 0xF5, 0xFF, + 0x26, 0xBA, 0xFB, 0xFF, 0x53, 0xB0, 0xB1, 0xFF, 0x9A, 0xC5, 0x06, 0xFF, 0xC0, 0xD2, 0x22, 0xFF, 0xD3, 0xDD, 0x36, + 0xFF, 0xB3, 0xBA, 0x12, 0xFF, 0xC3, 0xC7, 0x1E, 0xFF, 0xC4, 0xCE, 0x21, 0xFF, 0xD8, 0xD8, 0x2C, 0xFF, 0xDE, 0xDA, + 0x2F, 0xFF, 0xDC, 0xD5, 0x2A, 0xFF, 0xE7, 0xD4, 0x20, 0xFF, 0xD4, 0xD5, 0x1C, 0xFF, 0xE8, 0xE4, 0x28, 0xFF, 0xEB, + 0xE3, 0x24, 0xFF, 0xD1, 0xCD, 0x1F, 0xFF, 0xD2, 0xC5, 0x1C, 0xFF, 0xDC, 0xC2, 0x01, 0xFF, 0xCF, 0xC3, 0x11, 0xFF, + 0xE2, 0xC1, 0x09, 0xFF, 0xE3, 0xBE, 0x00, 0xFF, 0x83, 0xBE, 0x6E, 0xFF, 0x0C, 0x9F, 0xF6, 0xFF, 0x11, 0x9F, 0xFD, + 0xFF, 0x17, 0xA1, 0xF6, 0xFF, 0x19, 0xA1, 0xF4, 0xFF, 0x1A, 0xA1, 0xF3, 0xFF, 0x18, 0xA0, 0xF5, 0xFF, 0x15, 0x9F, + 0xF8, 0xFF, 0x15, 0x9E, 0xF7, 0xFF, 0x14, 0x9D, 0xF7, 0xFF, 0x14, 0x9C, 0xF7, 0xFF, 0x13, 0x9B, 0xF7, 0xFF, 0x11, + 0x99, 0xF5, 0xFF, 0x10, 0x98, 0xF4, 0xFF, 0x10, 0x98, 0xF4, 0xFF, 0x64, 0xD6, 0xFB, 0xFF, 0x5D, 0xD4, 0xF9, 0xFF, + 0x55, 0xD2, 0xF8, 0xFF, 0x53, 0xD0, 0xF8, 0xFF, 0x50, 0xCE, 0xF8, 0xFF, 0x4E, 0xCE, 0xF9, 0xFF, 0x4D, 0xCD, 0xFA, + 0xFF, 0x4B, 0xCC, 0xFA, 0xFF, 0x49, 0xCC, 0xFB, 0xFF, 0x48, 0xCB, 0xFA, 0xFF, 0x47, 0xCA, 0xFA, 0xFF, 0x46, 0xC9, + 0xFA, 0xFF, 0x45, 0xC8, 0xFA, 0xFF, 0x43, 0xC7, 0xFA, 0xFF, 0x42, 0xC6, 0xF9, 0xFF, 0x41, 0xC5, 0xF9, 0xFF, 0x40, + 0xC4, 0xF9, 0xFF, 0x3F, 0xC3, 0xF9, 0xFF, 0x3D, 0xC2, 0xF9, 0xFF, 0x3C, 0xC1, 0xF9, 0xFF, 0x3B, 0xC0, 0xF8, 0xFF, + 0x3A, 0xC0, 0xF8, 0xFF, 0x39, 0xBF, 0xF8, 0xFF, 0x38, 0xBF, 0xF8, 0xFF, 0x36, 0xBF, 0xF8, 0xFF, 0x34, 0xBD, 0xF7, + 0xFF, 0x31, 0xBC, 0xF7, 0xFF, 0x33, 0xBB, 0xF8, 0xFF, 0x35, 0xBA, 0xF9, 0xFF, 0x2C, 0xBC, 0xFF, 0xFF, 0x60, 0xC2, + 0xDE, 0xFF, 0x93, 0xCB, 0x84, 0xFF, 0xC5, 0xD4, 0x2A, 0xFF, 0xCA, 0xD7, 0x2E, 0xFF, 0xB0, 0xBA, 0x12, 0xFF, 0xB4, + 0xBE, 0x16, 0xFF, 0xB8, 0xC2, 0x1A, 0xFF, 0xC6, 0xC8, 0x25, 0xFF, 0xC4, 0xBE, 0x20, 0xFF, 0xDA, 0xC8, 0x16, 0xFF, + 0xC9, 0xC8, 0x18, 0xFF, 0xDB, 0xD7, 0x21, 0xFF, 0xDD, 0xD6, 0x1A, 0xFF, 0xB7, 0xBC, 0x0D, 0xFF, 0xC7, 0xBD, 0x03, + 0xFF, 0xD0, 0xBF, 0x00, 0xFF, 0xAC, 0xC9, 0x50, 0xFF, 0x6B, 0xB8, 0xB0, 0xFF, 0x04, 0xA3, 0xFF, 0xFF, 0x12, 0xA3, + 0xFA, 0xFF, 0x21, 0xA4, 0xF4, 0xFF, 0x1D, 0xA2, 0xF5, 0xFF, 0x19, 0xA1, 0xF5, 0xFF, 0x18, 0xA0, 0xF6, 0xFF, 0x17, + 0xA0, 0xF6, 0xFF, 0x16, 0x9F, 0xF7, 0xFF, 0x15, 0x9F, 0xF7, 0xFF, 0x14, 0x9E, 0xF7, 0xFF, 0x14, 0x9D, 0xF7, 0xFF, + 0x13, 0x9C, 0xF6, 0xFF, 0x12, 0x9B, 0xF6, 0xFF, 0x10, 0x99, 0xF4, 0xFF, 0x0E, 0x97, 0xF2, 0xFF, 0x0E, 0x97, 0xF2, + 0xFF, 0x5C, 0xD4, 0xF8, 0xFF, 0x57, 0xD3, 0xF8, 0xFF, 0x53, 0xD1, 0xF8, 0xFF, 0x51, 0xD0, 0xF8, 0xFF, 0x4F, 0xCE, + 0xF9, 0xFF, 0x4D, 0xCE, 0xF9, 0xFF, 0x4B, 0xCD, 0xF9, 0xFF, 0x4A, 0xCC, 0xFA, 0xFF, 0x48, 0xCB, 0xFA, 0xFF, 0x47, + 0xCA, 0xFA, 0xFF, 0x46, 0xCA, 0xFA, 0xFF, 0x45, 0xC9, 0xF9, 0xFF, 0x44, 0xC8, 0xF9, 0xFF, 0x43, 0xC7, 0xF9, 0xFF, + 0x42, 0xC6, 0xF9, 0xFF, 0x41, 0xC5, 0xF9, 0xFF, 0x40, 0xC4, 0xF9, 0xFF, 0x3E, 0xC3, 0xF9, 0xFF, 0x3D, 0xC2, 0xF9, + 0xFF, 0x3C, 0xC1, 0xF9, 0xFF, 0x3A, 0xC0, 0xF9, 0xFF, 0x39, 0xBF, 0xF8, 0xFF, 0x38, 0xBF, 0xF8, 0xFF, 0x37, 0xBF, + 0xF8, 0xFF, 0x36, 0xBE, 0xF8, 0xFF, 0x35, 0xBD, 0xF5, 0xFF, 0x34, 0xBB, 0xF3, 0xFF, 0x34, 0xB9, 0xF7, 0xFF, 0x34, + 0xB7, 0xFA, 0xFF, 0x22, 0xB5, 0xFF, 0xFF, 0x2E, 0xB4, 0xFE, 0xFF, 0x4D, 0xB9, 0xE6, 0xFF, 0x6B, 0xBF, 0xCD, 0xFF, + 0x27, 0xB1, 0xC5, 0xFF, 0x6C, 0xBB, 0x7C, 0xFF, 0x89, 0xBD, 0x48, 0xFF, 0xA6, 0xBE, 0x15, 0xFF, 0xB9, 0xBF, 0x08, + 0xFF, 0xCB, 0xBF, 0x00, 0xFF, 0xDA, 0xC4, 0x3D, 0xFF, 0xBA, 0xCA, 0x20, 0xFF, 0xAD, 0xC6, 0x3E, 0xFF, 0x99, 0xBB, + 0x53, 0xFF, 0x59, 0xAC, 0x8A, 0xFF, 0x35, 0xAA, 0xC3, 0xFF, 0x03, 0xB3, 0xFF, 0xFF, 0x15, 0xA6, 0xFF, 0xFF, 0x20, + 0xA4, 0xFF, 0xFF, 0x19, 0xA0, 0xFA, 0xFF, 0x1B, 0xA2, 0xF9, 0xFF, 0x1C, 0xA4, 0xF8, 0xFF, 0x1B, 0xA2, 0xF7, 0xFF, + 0x19, 0xA1, 0xF6, 0xFF, 0x18, 0xA0, 0xF6, 0xFF, 0x17, 0xA0, 0xF7, 0xFF, 0x16, 0x9F, 0xF7, 0xFF, 0x15, 0x9F, 0xF7, + 0xFF, 0x14, 0x9E, 0xF7, 0xFF, 0x14, 0x9D, 0xF6, 0xFF, 0x13, 0x9C, 0xF6, 0xFF, 0x12, 0x9B, 0xF6, 0xFF, 0x10, 0x9A, + 0xF4, 0xFF, 0x0F, 0x98, 0xF3, 0xFF, 0x0F, 0x98, 0xF3, 0xFF, 0x53, 0xD2, 0xF6, 0xFF, 0x52, 0xD1, 0xF7, 0xFF, 0x51, + 0xD0, 0xF8, 0xFF, 0x4F, 0xCF, 0xF8, 0xFF, 0x4E, 0xCE, 0xF9, 0xFF, 0x4C, 0xCE, 0xF9, 0xFF, 0x4A, 0xCD, 0xF9, 0xFF, + 0x48, 0xCC, 0xF9, 0xFF, 0x46, 0xCB, 0xF9, 0xFF, 0x46, 0xCA, 0xF9, 0xFF, 0x45, 0xC9, 0xF9, 0xFF, 0x44, 0xC8, 0xF9, + 0xFF, 0x43, 0xC8, 0xF9, 0xFF, 0x42, 0xC7, 0xF9, 0xFF, 0x41, 0xC6, 0xF9, 0xFF, 0x41, 0xC5, 0xF9, 0xFF, 0x40, 0xC4, + 0xF9, 0xFF, 0x3E, 0xC3, 0xF9, 0xFF, 0x3D, 0xC2, 0xF9, 0xFF, 0x3B, 0xC1, 0xF9, 0xFF, 0x3A, 0xC0, 0xF9, 0xFF, 0x38, + 0xBF, 0xF9, 0xFF, 0x37, 0xBF, 0xF8, 0xFF, 0x36, 0xBE, 0xF8, 0xFF, 0x35, 0xBE, 0xF8, 0xFF, 0x36, 0xBC, 0xF4, 0xFF, + 0x37, 0xBA, 0xEF, 0xFF, 0x35, 0xB7, 0xF5, 0xFF, 0x34, 0xB5, 0xFC, 0xFF, 0x2B, 0xB5, 0xF8, 0xFF, 0x22, 0xB6, 0xF5, + 0xFF, 0x25, 0xB5, 0xFA, 0xFF, 0x28, 0xB3, 0xFF, 0xFF, 0x28, 0xB5, 0xFF, 0xFF, 0x28, 0xB7, 0xFF, 0xFF, 0x1E, 0xB4, + 0xFF, 0xFF, 0x14, 0xB2, 0xFE, 0xFF, 0x20, 0xAD, 0xF6, 0xFF, 0x3C, 0xB9, 0xFE, 0xFF, 0x5A, 0xCB, 0xF0, 0xFF, 0x41, + 0xBE, 0xFA, 0xFF, 0x29, 0xB5, 0xFC, 0xFF, 0x11, 0xAD, 0xFE, 0xFF, 0x17, 0xAC, 0xFC, 0xFF, 0x1D, 0xAB, 0xFA, 0xFF, + 0x1D, 0xA9, 0xFD, 0xFF, 0x1D, 0xA7, 0xFF, 0xFF, 0x1B, 0xA7, 0xFA, 0xFF, 0x18, 0xA8, 0xF4, 0xFF, 0x18, 0xA6, 0xF8, + 0xFF, 0x17, 0xA4, 0xFC, 0xFF, 0x19, 0xA2, 0xFA, 0xFF, 0x1A, 0xA1, 0xF7, 0xFF, 0x19, 0xA0, 0xF7, 0xFF, 0x17, 0xA0, + 0xF7, 0xFF, 0x16, 0x9F, 0xF7, 0xFF, 0x15, 0x9F, 0xF7, 0xFF, 0x14, 0x9E, 0xF7, 0xFF, 0x13, 0x9D, 0xF6, 0xFF, 0x13, + 0x9C, 0xF6, 0xFF, 0x12, 0x9B, 0xF5, 0xFF, 0x11, 0x9A, 0xF4, 0xFF, 0x10, 0x99, 0xF3, 0xFF, 0x10, 0x99, 0xF3, 0xFF, + 0x54, 0xD1, 0xF7, 0xFF, 0x52, 0xD0, 0xF8, 0xFF, 0x51, 0xD0, 0xF8, 0xFF, 0x4F, 0xCF, 0xF9, 0xFF, 0x4E, 0xCF, 0xF9, + 0xFF, 0x4B, 0xCE, 0xF9, 0xFF, 0x49, 0xCD, 0xF9, 0xFF, 0x47, 0xCC, 0xF9, 0xFF, 0x45, 0xCA, 0xF8, 0xFF, 0x44, 0xCA, + 0xF8, 0xFF, 0x44, 0xC9, 0xF8, 0xFF, 0x43, 0xC8, 0xF9, 0xFF, 0x42, 0xC7, 0xF9, 0xFF, 0x42, 0xC7, 0xF9, 0xFF, 0x41, + 0xC6, 0xF9, 0xFF, 0x40, 0xC5, 0xF9, 0xFF, 0x40, 0xC4, 0xF9, 0xFF, 0x3E, 0xC3, 0xF9, 0xFF, 0x3C, 0xC2, 0xF9, 0xFF, + 0x3B, 0xC1, 0xF9, 0xFF, 0x39, 0xC0, 0xF9, 0xFF, 0x38, 0xBF, 0xF9, 0xFF, 0x36, 0xBE, 0xF8, 0xFF, 0x35, 0xBE, 0xF8, + 0xFF, 0x34, 0xBD, 0xF8, 0xFF, 0x34, 0xBC, 0xF6, 0xFF, 0x35, 0xBA, 0xF4, 0xFF, 0x34, 0xB8, 0xF8, 0xFF, 0x33, 0xB6, + 0xFB, 0xFF, 0x2D, 0xB6, 0xF9, 0xFF, 0x28, 0xB6, 0xF6, 0xFF, 0x29, 0xB5, 0xF8, 0xFF, 0x29, 0xB4, 0xFA, 0xFF, 0x29, + 0xB4, 0xFB, 0xFF, 0x29, 0xB5, 0xFC, 0xFF, 0x29, 0xB2, 0xF5, 0xFF, 0x29, 0xAF, 0xEF, 0xFF, 0x1A, 0xA9, 0xF5, 0xFF, + 0x9A, 0xCE, 0xD9, 0xFF, 0x6C, 0xCF, 0xE8, 0xFF, 0x73, 0xC6, 0xE3, 0xFF, 0x7F, 0xC9, 0xDD, 0xFF, 0x18, 0xAD, 0xFB, + 0xFF, 0x1B, 0xAC, 0xF9, 0xFF, 0x1F, 0xAB, 0xF7, 0xFF, 0x1E, 0xA9, 0xF9, 0xFF, 0x1D, 0xA7, 0xFB, 0xFF, 0x1C, 0xA7, + 0xF8, 0xFF, 0x1A, 0xA6, 0xF6, 0xFF, 0x19, 0xA5, 0xF8, 0xFF, 0x19, 0xA3, 0xFA, 0xFF, 0x1A, 0xA2, 0xF9, 0xFF, 0x1A, + 0xA1, 0xF8, 0xFF, 0x19, 0xA1, 0xF8, 0xFF, 0x18, 0xA0, 0xF8, 0xFF, 0x16, 0x9F, 0xF7, 0xFF, 0x15, 0x9F, 0xF7, 0xFF, + 0x14, 0x9E, 0xF7, 0xFF, 0x13, 0x9D, 0xF6, 0xFF, 0x12, 0x9C, 0xF6, 0xFF, 0x11, 0x9B, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, + 0xFF, 0x10, 0x9A, 0xF4, 0xFF, 0x10, 0x9A, 0xF4, 0xFF, 0x54, 0xD0, 0xF9, 0xFF, 0x52, 0xCF, 0xF9, 0xFF, 0x51, 0xCF, + 0xF9, 0xFF, 0x4F, 0xCF, 0xFA, 0xFF, 0x4D, 0xCF, 0xFA, 0xFF, 0x4A, 0xCE, 0xF9, 0xFF, 0x48, 0xCC, 0xF9, 0xFF, 0x46, + 0xCB, 0xF8, 0xFF, 0x43, 0xCA, 0xF8, 0xFF, 0x43, 0xC9, 0xF8, 0xFF, 0x43, 0xC9, 0xF8, 0xFF, 0x42, 0xC8, 0xF8, 0xFF, + 0x42, 0xC7, 0xF8, 0xFF, 0x41, 0xC7, 0xF8, 0xFF, 0x41, 0xC6, 0xF9, 0xFF, 0x40, 0xC5, 0xF9, 0xFF, 0x40, 0xC4, 0xF9, + 0xFF, 0x3E, 0xC3, 0xF9, 0xFF, 0x3C, 0xC2, 0xF9, 0xFF, 0x3A, 0xC1, 0xF9, 0xFF, 0x38, 0xBF, 0xF9, 0xFF, 0x37, 0xBF, + 0xF9, 0xFF, 0x36, 0xBE, 0xF9, 0xFF, 0x34, 0xBE, 0xF8, 0xFF, 0x33, 0xBD, 0xF8, 0xFF, 0x33, 0xBC, 0xF9, 0xFF, 0x32, + 0xBA, 0xF9, 0xFF, 0x32, 0xB9, 0xFA, 0xFF, 0x31, 0xB7, 0xFB, 0xFF, 0x30, 0xB6, 0xF9, 0xFF, 0x2E, 0xB6, 0xF8, 0xFF, + 0x2C, 0xB5, 0xF6, 0xFF, 0x2A, 0xB4, 0xF4, 0xFF, 0x2A, 0xB3, 0xF5, 0xFF, 0x2A, 0xB2, 0xF6, 0xFF, 0x29, 0xB2, 0xF9, + 0xFF, 0x28, 0xB2, 0xFB, 0xFF, 0x30, 0xB2, 0xF6, 0xFF, 0x11, 0xA8, 0xFD, 0xFF, 0x7E, 0xD3, 0xE1, 0xFF, 0x58, 0xBB, + 0xE6, 0xFF, 0x15, 0xAA, 0xFB, 0xFF, 0x1F, 0xAD, 0xF7, 0xFF, 0x1F, 0xAB, 0xF6, 0xFF, 0x20, 0xAA, 0xF5, 0xFF, 0x1F, + 0xA9, 0xF6, 0xFF, 0x1E, 0xA7, 0xF6, 0xFF, 0x1D, 0xA6, 0xF7, 0xFF, 0x1B, 0xA5, 0xF8, 0xFF, 0x1B, 0xA4, 0xF8, 0xFF, + 0x1B, 0xA3, 0xF8, 0xFF, 0x1B, 0xA2, 0xF8, 0xFF, 0x1A, 0xA1, 0xF9, 0xFF, 0x19, 0xA1, 0xF8, 0xFF, 0x18, 0xA0, 0xF8, + 0xFF, 0x16, 0x9F, 0xF8, 0xFF, 0x15, 0x9F, 0xF7, 0xFF, 0x14, 0x9E, 0xF7, 0xFF, 0x13, 0x9D, 0xF6, 0xFF, 0x12, 0x9C, + 0xF6, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x54, + 0xD0, 0xF9, 0xFF, 0x52, 0xCF, 0xF9, 0xFF, 0x51, 0xCF, 0xF9, 0xFF, 0x4F, 0xCF, 0xFA, 0xFF, 0x4D, 0xCF, 0xFA, 0xFF, + 0x4A, 0xCE, 0xF9, 0xFF, 0x48, 0xCC, 0xF9, 0xFF, 0x46, 0xCB, 0xF8, 0xFF, 0x43, 0xCA, 0xF8, 0xFF, 0x43, 0xC9, 0xF8, + 0xFF, 0x43, 0xC9, 0xF8, 0xFF, 0x42, 0xC8, 0xF8, 0xFF, 0x42, 0xC7, 0xF8, 0xFF, 0x41, 0xC7, 0xF8, 0xFF, 0x41, 0xC6, + 0xF9, 0xFF, 0x40, 0xC5, 0xF9, 0xFF, 0x40, 0xC4, 0xF9, 0xFF, 0x3E, 0xC3, 0xF9, 0xFF, 0x3C, 0xC2, 0xF9, 0xFF, 0x3A, + 0xC1, 0xF9, 0xFF, 0x38, 0xBF, 0xF9, 0xFF, 0x37, 0xBF, 0xF9, 0xFF, 0x36, 0xBE, 0xF9, 0xFF, 0x34, 0xBE, 0xF8, 0xFF, + 0x33, 0xBD, 0xF8, 0xFF, 0x33, 0xBC, 0xF9, 0xFF, 0x32, 0xBA, 0xF9, 0xFF, 0x32, 0xB9, 0xFA, 0xFF, 0x31, 0xB7, 0xFB, + 0xFF, 0x30, 0xB6, 0xF9, 0xFF, 0x2E, 0xB6, 0xF8, 0xFF, 0x2C, 0xB5, 0xF6, 0xFF, 0x2A, 0xB4, 0xF4, 0xFF, 0x2A, 0xB3, + 0xF5, 0xFF, 0x2A, 0xB2, 0xF6, 0xFF, 0x2A, 0xB2, 0xF8, 0xFF, 0x29, 0xB1, 0xFA, 0xFF, 0x2D, 0xB5, 0xF4, 0xFF, 0x1D, + 0xB4, 0xF5, 0xFF, 0x23, 0x9B, 0xFF, 0xFF, 0x1F, 0xB5, 0xF2, 0xFF, 0x0B, 0xAB, 0xFB, 0xFF, 0x1E, 0xAC, 0xF6, 0xFF, + 0x1F, 0xAB, 0xF6, 0xFF, 0x20, 0xAA, 0xF5, 0xFF, 0x1F, 0xA9, 0xF6, 0xFF, 0x1E, 0xA7, 0xF6, 0xFF, 0x1D, 0xA6, 0xF7, + 0xFF, 0x1B, 0xA5, 0xF8, 0xFF, 0x1B, 0xA4, 0xF8, 0xFF, 0x1B, 0xA3, 0xF8, 0xFF, 0x1B, 0xA2, 0xF8, 0xFF, 0x1A, 0xA1, + 0xF9, 0xFF, 0x19, 0xA1, 0xF8, 0xFF, 0x18, 0xA0, 0xF8, 0xFF, 0x16, 0x9F, 0xF8, 0xFF, 0x15, 0x9F, 0xF7, 0xFF, 0x14, + 0x9E, 0xF7, 0xFF, 0x13, 0x9D, 0xF6, 0xFF, 0x12, 0x9C, 0xF6, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, + 0x11, 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x54, 0xD0, 0xF9, 0xFF, 0x52, 0xCF, 0xF9, 0xFF, 0x51, 0xCF, 0xF9, + 0xFF, 0x4F, 0xCF, 0xFA, 0xFF, 0x4D, 0xCF, 0xFA, 0xFF, 0x4A, 0xCE, 0xF9, 0xFF, 0x48, 0xCC, 0xF9, 0xFF, 0x46, 0xCB, + 0xF8, 0xFF, 0x43, 0xCA, 0xF8, 0xFF, 0x43, 0xC9, 0xF8, 0xFF, 0x43, 0xC9, 0xF8, 0xFF, 0x42, 0xC8, 0xF8, 0xFF, 0x42, + 0xC7, 0xF8, 0xFF, 0x41, 0xC7, 0xF8, 0xFF, 0x41, 0xC6, 0xF9, 0xFF, 0x40, 0xC5, 0xF9, 0xFF, 0x40, 0xC4, 0xF9, 0xFF, + 0x3E, 0xC3, 0xF9, 0xFF, 0x3C, 0xC2, 0xF9, 0xFF, 0x3A, 0xC1, 0xF9, 0xFF, 0x38, 0xBF, 0xF9, 0xFF, 0x37, 0xBF, 0xF9, + 0xFF, 0x36, 0xBE, 0xF9, 0xFF, 0x34, 0xBE, 0xF8, 0xFF, 0x33, 0xBD, 0xF8, 0xFF, 0x33, 0xBC, 0xF9, 0xFF, 0x32, 0xBA, + 0xF9, 0xFF, 0x32, 0xB9, 0xFA, 0xFF, 0x31, 0xB7, 0xFB, 0xFF, 0x30, 0xB6, 0xF9, 0xFF, 0x2E, 0xB6, 0xF8, 0xFF, 0x2C, + 0xB5, 0xF6, 0xFF, 0x2A, 0xB4, 0xF4, 0xFF, 0x2A, 0xB3, 0xF5, 0xFF, 0x2A, 0xB2, 0xF6, 0xFF, 0x2A, 0xB2, 0xF7, 0xFF, + 0x2A, 0xB1, 0xF8, 0xFF, 0x21, 0xAE, 0xF9, 0xFF, 0x18, 0xAC, 0xFA, 0xFF, 0x1E, 0xAD, 0xF6, 0xFF, 0x23, 0xAE, 0xF3, + 0xFF, 0x20, 0xAC, 0xF4, 0xFF, 0x1D, 0xAB, 0xF5, 0xFF, 0x1E, 0xAA, 0xF5, 0xFF, 0x20, 0xAA, 0xF5, 0xFF, 0x1F, 0xA9, + 0xF6, 0xFF, 0x1E, 0xA7, 0xF6, 0xFF, 0x1D, 0xA6, 0xF7, 0xFF, 0x1B, 0xA5, 0xF8, 0xFF, 0x1B, 0xA4, 0xF8, 0xFF, 0x1B, + 0xA3, 0xF8, 0xFF, 0x1B, 0xA2, 0xF8, 0xFF, 0x1A, 0xA1, 0xF9, 0xFF, 0x19, 0xA1, 0xF8, 0xFF, 0x18, 0xA0, 0xF8, 0xFF, + 0x16, 0x9F, 0xF8, 0xFF, 0x15, 0x9F, 0xF7, 0xFF, 0x14, 0x9E, 0xF7, 0xFF, 0x13, 0x9D, 0xF6, 0xFF, 0x12, 0x9C, 0xF6, + 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x54, 0xD0, + 0xF9, 0xFF, 0x52, 0xCF, 0xF9, 0xFF, 0x51, 0xCF, 0xF9, 0xFF, 0x4F, 0xCF, 0xFA, 0xFF, 0x4D, 0xCF, 0xFA, 0xFF, 0x4A, + 0xCE, 0xF9, 0xFF, 0x48, 0xCC, 0xF9, 0xFF, 0x46, 0xCB, 0xF8, 0xFF, 0x43, 0xCA, 0xF8, 0xFF, 0x43, 0xC9, 0xF8, 0xFF, + 0x43, 0xC9, 0xF8, 0xFF, 0x42, 0xC8, 0xF8, 0xFF, 0x42, 0xC7, 0xF8, 0xFF, 0x41, 0xC7, 0xF8, 0xFF, 0x41, 0xC6, 0xF9, + 0xFF, 0x40, 0xC5, 0xF9, 0xFF, 0x40, 0xC4, 0xF9, 0xFF, 0x3E, 0xC3, 0xF9, 0xFF, 0x3C, 0xC2, 0xF9, 0xFF, 0x3A, 0xC1, + 0xF9, 0xFF, 0x38, 0xBF, 0xF9, 0xFF, 0x37, 0xBF, 0xF9, 0xFF, 0x36, 0xBE, 0xF9, 0xFF, 0x34, 0xBE, 0xF8, 0xFF, 0x33, + 0xBD, 0xF8, 0xFF, 0x33, 0xBC, 0xF9, 0xFF, 0x32, 0xBA, 0xF9, 0xFF, 0x32, 0xB9, 0xFA, 0xFF, 0x31, 0xB7, 0xFB, 0xFF, + 0x30, 0xB6, 0xF9, 0xFF, 0x2E, 0xB6, 0xF8, 0xFF, 0x2C, 0xB5, 0xF6, 0xFF, 0x2A, 0xB4, 0xF4, 0xFF, 0x2A, 0xB3, 0xF5, + 0xFF, 0x2A, 0xB2, 0xF6, 0xFF, 0x2A, 0xB2, 0xF7, 0xFF, 0x2A, 0xB1, 0xF8, 0xFF, 0x21, 0xAE, 0xF9, 0xFF, 0x18, 0xAC, + 0xFA, 0xFF, 0x1E, 0xAD, 0xF6, 0xFF, 0x23, 0xAE, 0xF3, 0xFF, 0x20, 0xAC, 0xF4, 0xFF, 0x1D, 0xAB, 0xF5, 0xFF, 0x1E, + 0xAA, 0xF5, 0xFF, 0x20, 0xAA, 0xF5, 0xFF, 0x1F, 0xA9, 0xF6, 0xFF, 0x1E, 0xA7, 0xF6, 0xFF, 0x1D, 0xA6, 0xF7, 0xFF, + 0x1B, 0xA5, 0xF8, 0xFF, 0x1B, 0xA4, 0xF8, 0xFF, 0x1B, 0xA3, 0xF8, 0xFF, 0x1B, 0xA2, 0xF8, 0xFF, 0x1A, 0xA1, 0xF9, + 0xFF, 0x19, 0xA1, 0xF8, 0xFF, 0x18, 0xA0, 0xF8, 0xFF, 0x16, 0x9F, 0xF8, 0xFF, 0x15, 0x9F, 0xF7, 0xFF, 0x14, 0x9E, + 0xF7, 0xFF, 0x13, 0x9D, 0xF6, 0xFF, 0x12, 0x9C, 0xF6, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, 0xFF, 0x11, + 0x9A, 0xF5, 0xFF, 0x11, 0x9A, 0xF5, +]; + +const CONVERTED_TO_BGRX_WITH_PARTIALLY_COVERED_RECTANGLE_BUFFER: [u8; 64 * 64 * 4] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE4, 0xAB, 0x2E, 0xFF, 0xE6, 0xAC, 0x2E, 0xFF, 0xE8, 0xAD, + 0x2E, 0xFF, 0xE7, 0xAB, 0x2D, 0xFF, 0xE5, 0xAA, 0x2C, 0xFF, 0xFF, 0xB2, 0x15, 0xFF, 0x10, 0x42, 0xEB, 0xFF, 0x16, + 0x4F, 0xF1, 0xFF, 0x1C, 0x5C, 0xF7, 0xFF, 0x23, 0x71, 0xF8, 0xFF, 0x29, 0x85, 0xF9, 0xFF, 0x2D, 0x88, 0xF6, 0xFF, + 0x30, 0x8B, 0xF3, 0xFF, 0x31, 0x85, 0xF4, 0xFF, 0x33, 0x7F, 0xF4, 0xFF, 0x35, 0x85, 0xF6, 0xFF, 0x37, 0x8B, 0xF9, + 0xFF, 0x38, 0x8D, 0xF8, 0xFF, 0x3A, 0x90, 0xF7, 0xFF, 0x37, 0x8B, 0xF8, 0xFF, 0x35, 0x86, 0xF8, 0xFF, 0x35, 0x7E, + 0xF7, 0xFF, 0x35, 0x75, 0xF6, 0xFF, 0x33, 0x6D, 0xF7, 0xFF, 0x31, 0x64, 0xF7, 0xFF, 0x31, 0x5E, 0xF8, 0xFF, 0x30, + 0x57, 0xF8, 0xFF, 0x25, 0x51, 0xFF, 0xFF, 0x36, 0x51, 0xF5, 0xFF, 0xFD, 0xA4, 0x03, 0xFF, 0xE1, 0x9A, 0x1E, 0xFF, + 0xE3, 0x98, 0x1E, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, + 0xAD, 0x30, 0xFF, 0xE8, 0xAE, 0x2F, 0xFF, 0xEA, 0xB0, 0x2D, 0xFF, 0xEC, 0xAD, 0x30, 0xFF, 0xEE, 0xAF, 0x28, 0xFF, + 0xC8, 0xA9, 0x2F, 0xFF, 0x04, 0x3D, 0xFF, 0xFF, 0x19, 0x50, 0xFA, 0xFF, 0x21, 0x5F, 0xF8, 0xFF, 0x28, 0x73, 0xF7, + 0xFF, 0x2F, 0x87, 0xF7, 0xFF, 0x37, 0x95, 0xFA, 0xFF, 0x37, 0x9B, 0xF5, 0xFF, 0x3A, 0x96, 0xF5, 0xFF, 0x3D, 0x92, + 0xF5, 0xFF, 0x3F, 0x94, 0xF7, 0xFF, 0x41, 0x96, 0xF9, 0xFF, 0x43, 0x99, 0xF9, 0xFF, 0x46, 0x9D, 0xF9, 0xFF, 0x44, + 0x98, 0xF8, 0xFF, 0x43, 0x94, 0xF7, 0xFF, 0x42, 0x8D, 0xF8, 0xFF, 0x41, 0x86, 0xF9, 0xFF, 0x3F, 0x7D, 0xF9, 0xFF, + 0x3C, 0x73, 0xF9, 0xFF, 0x38, 0x70, 0xF7, 0xFF, 0x35, 0x6C, 0xF4, 0xFF, 0x21, 0x60, 0xFF, 0xFF, 0x62, 0x6C, 0xBE, + 0xFF, 0xEF, 0x9D, 0x12, 0xFF, 0xE8, 0x9A, 0x21, 0xFF, 0xED, 0x99, 0x1C, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE8, 0xAF, 0x32, 0xFF, 0xEA, 0xB1, 0x30, 0xFF, 0xEC, 0xB4, 0x2D, + 0xFF, 0xF1, 0xAE, 0x34, 0xFF, 0xF6, 0xB4, 0x24, 0xFF, 0x86, 0x7E, 0x8D, 0xFF, 0x00, 0x4E, 0xF6, 0xFF, 0x1D, 0x5C, + 0xEC, 0xFF, 0x25, 0x63, 0xF9, 0xFF, 0x2D, 0x76, 0xF7, 0xFF, 0x35, 0x89, 0xF4, 0xFF, 0x41, 0xA2, 0xFD, 0xFF, 0x3E, + 0xAB, 0xF6, 0xFF, 0x43, 0xA8, 0xF6, 0xFF, 0x47, 0xA4, 0xF7, 0xFF, 0x4A, 0xA3, 0xF8, 0xFF, 0x4C, 0xA1, 0xFA, 0xFF, + 0x4E, 0xA5, 0xFA, 0xFF, 0x51, 0xAA, 0xFB, 0xFF, 0x52, 0xA6, 0xF9, 0xFF, 0x52, 0xA2, 0xF7, 0xFF, 0x4F, 0x9C, 0xFA, + 0xFF, 0x4D, 0x97, 0xFD, 0xFF, 0x4A, 0x8D, 0xFC, 0xFF, 0x47, 0x83, 0xFB, 0xFF, 0x40, 0x82, 0xF6, 0xFF, 0x39, 0x82, + 0xF1, 0xFF, 0x2B, 0x72, 0xF4, 0xFF, 0xAB, 0x8C, 0x71, 0xFF, 0xF0, 0x99, 0x16, 0xFF, 0xEF, 0x99, 0x25, 0xFF, 0xE8, + 0x97, 0x25, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEA, 0xB0, + 0x34, 0xFF, 0xE9, 0xB3, 0x32, 0xFF, 0xE8, 0xB5, 0x2F, 0xFF, 0xF0, 0xB0, 0x34, 0xFF, 0xF8, 0xB6, 0x22, 0xFF, 0x44, + 0x60, 0xC5, 0xFF, 0x0B, 0x53, 0xF9, 0xFF, 0x21, 0x63, 0xF2, 0xFF, 0x29, 0x6F, 0xF6, 0xFF, 0x2F, 0x7D, 0xF6, 0xFF, + 0x35, 0x8A, 0xF7, 0xFF, 0x41, 0xA1, 0xFA, 0xFF, 0x45, 0xAF, 0xF6, 0xFF, 0x4F, 0xB4, 0xFA, 0xFF, 0x50, 0xB0, 0xF6, + 0xFF, 0x53, 0xAE, 0xF8, 0xFF, 0x56, 0xAC, 0xFA, 0xFF, 0x59, 0xB2, 0xFC, 0xFF, 0x5D, 0xB7, 0xFD, 0xFF, 0x5F, 0xB3, + 0xFA, 0xFF, 0x61, 0xAF, 0xF6, 0xFF, 0x5D, 0xAC, 0xF9, 0xFF, 0x59, 0xA9, 0xFD, 0xFF, 0x55, 0x9F, 0xFB, 0xFF, 0x50, + 0x94, 0xF8, 0xFF, 0x4A, 0x91, 0xF7, 0xFF, 0x44, 0x8D, 0xF5, 0xFF, 0x22, 0x7D, 0xFF, 0xFF, 0xEF, 0xA5, 0x1A, 0xFF, + 0xF3, 0x9E, 0x12, 0xFF, 0xF1, 0x96, 0x28, 0xFF, 0xB0, 0x9F, 0x22, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEC, 0xB2, 0x36, 0xFF, 0xE8, 0xB4, 0x33, 0xFF, 0xE4, 0xB5, 0x31, 0xFF, + 0xEF, 0xB1, 0x34, 0xFF, 0xF9, 0xB8, 0x21, 0xFF, 0x02, 0x41, 0xFD, 0xFF, 0x1E, 0x58, 0xFC, 0xFF, 0x25, 0x6A, 0xF8, + 0xFF, 0x2C, 0x7C, 0xF3, 0xFF, 0x31, 0x84, 0xF6, 0xFF, 0x35, 0x8B, 0xF9, 0xFF, 0x41, 0xA0, 0xF7, 0xFF, 0x4C, 0xB4, + 0xF6, 0xFF, 0x5B, 0xC0, 0xFE, 0xFF, 0x59, 0xBC, 0xF6, 0xFF, 0x5D, 0xBA, 0xF8, 0xFF, 0x60, 0xB7, 0xFA, 0xFF, 0x64, + 0xBE, 0xFD, 0xFF, 0x69, 0xC4, 0xFF, 0xFF, 0x6C, 0xC0, 0xFA, 0xFF, 0x6F, 0xBD, 0xF5, 0xFF, 0x6A, 0xBC, 0xF9, 0xFF, + 0x65, 0xBB, 0xFD, 0xFF, 0x60, 0xB1, 0xFA, 0xFF, 0x5A, 0xA6, 0xF6, 0xFF, 0x54, 0x9F, 0xF8, 0xFF, 0x4F, 0x98, 0xFA, + 0xFF, 0x6E, 0x94, 0xDF, 0xFF, 0xFB, 0xA6, 0x07, 0xFF, 0xDA, 0x9C, 0x24, 0xFF, 0xF2, 0x9F, 0x14, 0xFF, 0x71, 0xA1, + 0x4A, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xED, 0xB6, 0x39, + 0xFF, 0xEC, 0xB4, 0x37, 0xFF, 0xEB, 0xB2, 0x34, 0xFF, 0xF2, 0xAB, 0x34, 0xFF, 0xB3, 0x95, 0x6D, 0xFF, 0x00, 0x46, + 0xFF, 0xFF, 0x20, 0x64, 0xF7, 0xFF, 0x28, 0x73, 0xF6, 0xFF, 0x30, 0x81, 0xF5, 0xFF, 0x37, 0x8B, 0xF6, 0xFF, 0x3D, + 0x94, 0xF8, 0xFF, 0x48, 0xA6, 0xF8, 0xFF, 0x53, 0xB7, 0xF7, 0xFF, 0x60, 0xC2, 0xFB, 0xFF, 0x65, 0xC4, 0xF7, 0xFF, + 0x69, 0xC3, 0xF9, 0xFF, 0x6D, 0xC2, 0xFA, 0xFF, 0x72, 0xC6, 0xFA, 0xFF, 0x77, 0xCB, 0xFA, 0xFF, 0x7A, 0xCB, 0xFB, + 0xFF, 0x7D, 0xCB, 0xFC, 0xFF, 0x7A, 0xC8, 0xFA, 0xFF, 0x77, 0xC5, 0xF8, 0xFF, 0x72, 0xBC, 0xF9, 0xFF, 0x6C, 0xB4, + 0xFA, 0xFF, 0x68, 0xB0, 0xF6, 0xFF, 0x56, 0xAA, 0xFD, 0xFF, 0xA5, 0xA0, 0x93, 0xFF, 0xF3, 0xA1, 0x13, 0xFF, 0xEF, + 0x9C, 0x21, 0xFF, 0xFF, 0x9D, 0x19, 0xFF, 0x23, 0xC1, 0x71, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEE, 0xB9, 0x3C, 0xFF, 0xF0, 0xB4, 0x3A, 0xFF, 0xF2, 0xAE, 0x37, 0xFF, 0xFE, + 0xB3, 0x32, 0xFF, 0x7C, 0x8E, 0xB3, 0xFF, 0x06, 0x58, 0xFF, 0xFF, 0x22, 0x71, 0xF3, 0xFF, 0x2B, 0x7C, 0xF4, 0xFF, + 0x34, 0x86, 0xF6, 0xFF, 0x3D, 0x92, 0xF7, 0xFF, 0x45, 0x9D, 0xF8, 0xFF, 0x4F, 0xAC, 0xF8, 0xFF, 0x5A, 0xBB, 0xF8, + 0xFF, 0x65, 0xC4, 0xF9, 0xFF, 0x70, 0xCC, 0xF9, 0xFF, 0x75, 0xCC, 0xFA, 0xFF, 0x7A, 0xCC, 0xFA, 0xFF, 0x80, 0xCF, + 0xF7, 0xFF, 0x85, 0xD2, 0xF4, 0xFF, 0x89, 0xD5, 0xFB, 0xFF, 0x8C, 0xD9, 0xFF, 0xFF, 0x8B, 0xD3, 0xFA, 0xFF, 0x89, + 0xCE, 0xF2, 0xFF, 0x84, 0xC8, 0xF8, 0xFF, 0x7F, 0xC1, 0xFE, 0xFF, 0x7C, 0xC1, 0xF4, 0xFF, 0x5E, 0xBC, 0xFF, 0xFF, + 0xDB, 0xAB, 0x47, 0xFF, 0xEA, 0x9C, 0x1E, 0xFF, 0xE8, 0xA2, 0x1D, 0xFF, 0xE5, 0xA7, 0x1D, 0xFF, 0x1B, 0xD3, 0x98, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEB, 0xB8, 0x3C, 0xFF, + 0xF0, 0xB3, 0x3F, 0xFF, 0xF4, 0xAF, 0x42, 0xFF, 0xE8, 0xBA, 0x0D, 0xFF, 0x96, 0xB8, 0xFF, 0xFF, 0x4C, 0x81, 0xF6, + 0xFF, 0x22, 0x75, 0xF5, 0xFF, 0x2D, 0x80, 0xF6, 0xFF, 0x38, 0x8B, 0xF7, 0xFF, 0x42, 0x99, 0xF7, 0xFF, 0x4D, 0xA6, + 0xF7, 0xFF, 0x56, 0xB2, 0xF8, 0xFF, 0x5F, 0xBD, 0xF9, 0xFF, 0x6D, 0xC8, 0xF9, 0xFF, 0x7A, 0xD4, 0xFA, 0xFF, 0x81, + 0xD5, 0xFA, 0xFF, 0x88, 0xD7, 0xF9, 0xFF, 0x8D, 0xD8, 0xFA, 0xFF, 0x92, 0xDA, 0xFB, 0xFF, 0xA1, 0xE4, 0xF9, 0xFF, + 0x91, 0xD6, 0xFE, 0xFF, 0x9F, 0xDE, 0xFA, 0xFF, 0x97, 0xDB, 0xF8, 0xFF, 0x93, 0xD5, 0xF9, 0xFF, 0x8F, 0xCF, 0xFB, + 0xFF, 0x85, 0xD1, 0xFF, 0xFF, 0x78, 0xC6, 0xFF, 0xFF, 0xFC, 0x9A, 0x00, 0xFF, 0xF1, 0xA8, 0x26, 0xFF, 0xF8, 0xA4, + 0x1F, 0xFF, 0xA5, 0xBD, 0x53, 0xFF, 0x30, 0xDA, 0xA4, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE8, 0xB7, 0x3B, 0xFF, 0xF0, 0xB9, 0x39, 0xFF, 0xF7, 0xBA, 0x37, 0xFF, 0xDC, 0xB5, + 0x50, 0xFF, 0x44, 0x96, 0xFF, 0xFF, 0x9C, 0xC4, 0xFE, 0xFF, 0x23, 0x79, 0xF7, 0xFF, 0x30, 0x85, 0xF8, 0xFF, 0x3C, + 0x91, 0xF8, 0xFF, 0x48, 0xA0, 0xF8, 0xFF, 0x55, 0xAF, 0xF7, 0xFF, 0x5D, 0xB7, 0xF8, 0xFF, 0x65, 0xBF, 0xF9, 0xFF, + 0x75, 0xCD, 0xFA, 0xFF, 0x85, 0xDB, 0xFB, 0xFF, 0x8D, 0xDE, 0xFA, 0xFF, 0x95, 0xE1, 0xF9, 0xFF, 0x9A, 0xE1, 0xFD, + 0xFF, 0xA0, 0xE2, 0xFF, 0xFF, 0xA3, 0xE8, 0xFA, 0xFF, 0x6B, 0xBD, 0xFF, 0xFF, 0x9E, 0xDE, 0xFC, 0xFF, 0xA6, 0xE8, + 0xFF, 0xFF, 0xA3, 0xE3, 0xFB, 0xFF, 0xA0, 0xDE, 0xF7, 0xFF, 0x99, 0xD7, 0xFD, 0xFF, 0xAB, 0xBD, 0xB5, 0xFF, 0xF0, + 0x9F, 0x11, 0xFF, 0xE8, 0xA3, 0x1D, 0xFF, 0xFF, 0x9E, 0x19, 0xFF, 0x65, 0xD4, 0x89, 0xFF, 0x45, 0xE1, 0xB0, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEE, 0xB8, 0x3C, 0xFF, 0xEB, + 0xB8, 0x37, 0xFF, 0xF6, 0xBC, 0x26, 0xFF, 0x8F, 0x9B, 0x94, 0xFF, 0x37, 0x96, 0xFB, 0xFF, 0x7C, 0xBB, 0xF9, 0xFF, + 0x85, 0xB5, 0xF8, 0xFF, 0x49, 0x99, 0xF6, 0xFF, 0x42, 0x9B, 0xF5, 0xFF, 0x4E, 0xA6, 0xF6, 0xFF, 0x59, 0xB2, 0xF7, + 0xFF, 0x65, 0xBC, 0xF8, 0xFF, 0x72, 0xC6, 0xF9, 0xFF, 0x7F, 0xD3, 0xF9, 0xFF, 0x8D, 0xE0, 0xFA, 0xFF, 0x97, 0xE5, + 0xF9, 0xFF, 0xA1, 0xEB, 0xF8, 0xFF, 0xA6, 0xEA, 0xFE, 0xFF, 0xAA, 0xEA, 0xFF, 0xFF, 0xA8, 0xEE, 0xFC, 0xFF, 0x62, + 0xBA, 0xF9, 0xFF, 0x98, 0xDC, 0xFA, 0xFF, 0xB9, 0xF3, 0xFE, 0xFF, 0xB2, 0xEC, 0xFB, 0xFF, 0xAB, 0xE5, 0xF7, 0xFF, + 0xA2, 0xE4, 0xFE, 0xFF, 0xD1, 0xB0, 0x64, 0xFF, 0xF0, 0x9F, 0x19, 0xFF, 0xE8, 0x9E, 0x26, 0xFF, 0xF2, 0x98, 0x03, + 0xFF, 0x50, 0xEF, 0xE3, 0xFF, 0x57, 0xEE, 0xD5, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xF3, 0xB9, 0x3C, 0xFF, 0xE6, 0xB8, 0x34, 0xFF, 0xF6, 0xBD, 0x16, 0xFF, 0x4F, 0x7F, 0xD8, + 0xFF, 0x46, 0x90, 0xF7, 0xFF, 0x54, 0xA5, 0xF7, 0xFF, 0xBA, 0xDA, 0xFF, 0xFF, 0x4D, 0xA1, 0xF8, 0xFF, 0x49, 0xA5, + 0xF3, 0xFF, 0x53, 0xAD, 0xF4, 0xFF, 0x5D, 0xB5, 0xF6, 0xFF, 0x6E, 0xC0, 0xF8, 0xFF, 0x7F, 0xCC, 0xFA, 0xFF, 0x8A, + 0xD8, 0xF9, 0xFF, 0x95, 0xE4, 0xF8, 0xFF, 0xA1, 0xEC, 0xF8, 0xFF, 0xAE, 0xF4, 0xF7, 0xFF, 0xB2, 0xF3, 0xFE, 0xFF, + 0xB5, 0xF1, 0xFF, 0xFF, 0xAD, 0xF4, 0xFE, 0xFF, 0x59, 0xB6, 0xF3, 0xFF, 0x92, 0xDA, 0xF8, 0xFF, 0xCC, 0xFF, 0xFE, + 0xFF, 0xC1, 0xF6, 0xFA, 0xFF, 0xB6, 0xED, 0xF7, 0xFF, 0xAB, 0xF1, 0xFF, 0xFF, 0xF7, 0xA4, 0x13, 0xFF, 0xEF, 0xA4, + 0x15, 0xFF, 0xE8, 0xA5, 0x18, 0xFF, 0xCD, 0xB4, 0x56, 0xFF, 0x71, 0xF2, 0xF0, 0xFF, 0x84, 0xEF, 0xD4, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xBB, 0x3E, 0xFF, 0xFD, 0xC0, + 0x2F, 0xFF, 0xFB, 0xBD, 0x35, 0xFF, 0x00, 0x4B, 0xF5, 0xFF, 0x52, 0x8A, 0xFF, 0xFF, 0x5D, 0xA5, 0xFA, 0xFF, 0x8D, + 0xC4, 0xFC, 0xFF, 0x85, 0xC1, 0xFB, 0xFF, 0x50, 0xAD, 0xF5, 0xFF, 0x5E, 0xB6, 0xF7, 0xFF, 0x6B, 0xBE, 0xF9, 0xFF, + 0x78, 0xC9, 0xFA, 0xFF, 0x85, 0xD4, 0xFB, 0xFF, 0x97, 0xDE, 0xFE, 0xFF, 0xAA, 0xE8, 0xFF, 0xFF, 0xAD, 0xEE, 0xFD, + 0xFF, 0xB1, 0xF4, 0xF9, 0xFF, 0xB9, 0xF5, 0xFC, 0xFF, 0xC2, 0xF6, 0xFE, 0xFF, 0xB2, 0xF0, 0xFB, 0xFF, 0x6E, 0xCB, + 0xF6, 0xFF, 0x91, 0xDE, 0xFB, 0xFF, 0xCA, 0xFC, 0xFC, 0xFF, 0xD0, 0xFB, 0xFF, 0xFF, 0xC8, 0xFC, 0xFF, 0xFF, 0xC7, + 0xE3, 0xCA, 0xFF, 0xF2, 0xA1, 0x15, 0xFF, 0xEE, 0xA3, 0x1D, 0xFF, 0xF1, 0xA1, 0x11, 0xFF, 0xB9, 0xD4, 0x9E, 0xFF, + 0x8B, 0xF1, 0xEA, 0xFF, 0x95, 0xEF, 0xDC, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xF0, 0xBD, 0x41, 0xFF, 0xF0, 0xBA, 0x37, 0xFF, 0xB7, 0xA1, 0x71, 0xFF, 0x1D, 0x5D, 0xFE, 0xFF, + 0x31, 0x79, 0xF8, 0xFF, 0x51, 0xA1, 0xF5, 0xFF, 0x60, 0xAD, 0xF8, 0xFF, 0xBC, 0xE0, 0xFE, 0xFF, 0x57, 0xB6, 0xF7, + 0xFF, 0x68, 0xBF, 0xF9, 0xFF, 0x79, 0xC8, 0xFC, 0xFF, 0x82, 0xD2, 0xFC, 0xFF, 0x8B, 0xDB, 0xFC, 0xFF, 0x8F, 0xDE, + 0xFB, 0xFF, 0x92, 0xE0, 0xFB, 0xFF, 0xA3, 0xEA, 0xFA, 0xFF, 0xB4, 0xF4, 0xFA, 0xFF, 0xC1, 0xF8, 0xF9, 0xFF, 0xCE, + 0xFB, 0xF8, 0xFF, 0xB6, 0xEB, 0xF9, 0xFF, 0x83, 0xE1, 0xFA, 0xFF, 0x8F, 0xE2, 0xFD, 0xFF, 0xC7, 0xF9, 0xFB, 0xFF, + 0xD7, 0xF8, 0xFC, 0xFF, 0xCA, 0xFC, 0xFE, 0xFF, 0xDC, 0xCD, 0x8B, 0xFF, 0xED, 0x9F, 0x18, 0xFF, 0xED, 0xA3, 0x24, + 0xFF, 0xFA, 0x9D, 0x0A, 0xFF, 0xA5, 0xF5, 0xE7, 0xFF, 0xA5, 0xF1, 0xE4, 0xFF, 0xA5, 0xF0, 0xE4, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF, 0xBC, 0x3F, 0xFF, 0xFD, 0xC2, 0x32, + 0xFF, 0x6E, 0x7F, 0xBD, 0xFF, 0x26, 0x65, 0xFE, 0xFF, 0x34, 0x7B, 0xF5, 0xFF, 0x4C, 0x9A, 0xF5, 0xFF, 0x5C, 0xAB, + 0xF8, 0xFF, 0x9F, 0xD0, 0xFA, 0xFF, 0x83, 0xC6, 0xF7, 0xFF, 0x6A, 0xC1, 0xFD, 0xFF, 0x7E, 0xD1, 0xFD, 0xFF, 0x87, + 0xDB, 0xFB, 0xFF, 0x8F, 0xE5, 0xF9, 0xFF, 0x9A, 0xEC, 0xF8, 0xFF, 0xA5, 0xF4, 0xF7, 0xFF, 0x99, 0xEA, 0xFB, 0xFF, + 0x8E, 0xDF, 0xFF, 0xFF, 0x9F, 0xE2, 0xFB, 0xFF, 0xB1, 0xE6, 0xF7, 0xFF, 0xCC, 0xED, 0xFB, 0xFF, 0xCA, 0xFA, 0xFF, + 0xFF, 0xC6, 0xF2, 0xFF, 0xFF, 0xC2, 0xF0, 0xFC, 0xFF, 0xD2, 0xF5, 0xFE, 0xFF, 0xD3, 0xFC, 0xFF, 0xFF, 0xE6, 0xB5, + 0x4B, 0xFF, 0xED, 0xA4, 0x20, 0xFF, 0xED, 0xA2, 0x1B, 0xFF, 0xE2, 0xAA, 0x3D, 0xFF, 0xAB, 0xF6, 0xEE, 0xFF, 0xB1, + 0xF1, 0xE5, 0xFF, 0xB4, 0xF2, 0xE7, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xEF, 0xBA, 0x3D, 0xFF, 0xFF, 0xCA, 0x2C, 0xFF, 0x24, 0x5D, 0xFF, 0xFF, 0x2E, 0x6D, 0xFE, 0xFF, 0x38, + 0x7D, 0xF2, 0xFF, 0x48, 0x93, 0xF5, 0xFF, 0x57, 0xA9, 0xF7, 0xFF, 0x82, 0xC0, 0xF7, 0xFF, 0xAE, 0xD7, 0xF7, 0xFF, + 0x6C, 0xC2, 0xFF, 0xFF, 0x84, 0xDA, 0xFE, 0xFF, 0x8B, 0xE4, 0xFA, 0xFF, 0x93, 0xEE, 0xF6, 0xFF, 0x9D, 0xED, 0xF8, + 0xFF, 0xA7, 0xEC, 0xF9, 0xFF, 0xB3, 0xF1, 0xF8, 0xFF, 0xC0, 0xF6, 0xF7, 0xFF, 0xC8, 0xF6, 0xFB, 0xFF, 0xD0, 0xF6, + 0xFF, 0xFF, 0xD3, 0xF2, 0xFE, 0xFF, 0xB9, 0xF3, 0xFB, 0xFF, 0xE7, 0xFD, 0xFF, 0xFF, 0xE9, 0xFD, 0xF6, 0xFF, 0xE2, + 0xFC, 0xFC, 0xFF, 0xDC, 0xFC, 0xFF, 0xFF, 0xF1, 0x9D, 0x0B, 0xFF, 0xEC, 0xAA, 0x29, 0xFF, 0xF5, 0xAA, 0x1B, 0xFF, + 0xD9, 0xC7, 0x7F, 0xFF, 0xBA, 0xFE, 0xFD, 0xFF, 0xBD, 0xF2, 0xE7, 0xFF, 0xC3, 0xF4, 0xEB, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF5, 0xC0, 0x39, 0xFF, 0xCA, 0xAC, 0x5E, 0xFF, + 0x1E, 0x58, 0xFA, 0xFF, 0x30, 0x6E, 0xF3, 0xFF, 0x35, 0x80, 0xF7, 0xFF, 0x3E, 0x92, 0xFB, 0xFF, 0x5D, 0xAF, 0xFB, + 0xFF, 0x72, 0xC2, 0xFF, 0xFF, 0xBA, 0xE1, 0xFD, 0xFF, 0x74, 0xCD, 0xFF, 0xFF, 0x71, 0xD3, 0xFF, 0xFF, 0x83, 0xE5, + 0xFF, 0xFF, 0x95, 0xF7, 0xFF, 0xFF, 0xA1, 0xF4, 0xFE, 0xFF, 0xAD, 0xF0, 0xFD, 0xFF, 0xC1, 0xF8, 0xFF, 0xFF, 0xCD, + 0xF7, 0xFB, 0xFF, 0xD1, 0xF8, 0xFE, 0xFF, 0xD6, 0xF9, 0xFF, 0xFF, 0xE0, 0xF6, 0xFE, 0xFF, 0xDD, 0xF5, 0xFB, 0xFF, + 0xED, 0xFB, 0xFF, 0xFF, 0xE8, 0xFB, 0xFB, 0xFF, 0xDF, 0xFC, 0xFF, 0xFF, 0xE8, 0xE0, 0xB2, 0xFF, 0xEF, 0xA3, 0x18, + 0xFF, 0xEC, 0xAA, 0x25, 0xFF, 0xF5, 0xA8, 0x15, 0xFF, 0xD8, 0xE3, 0xC2, 0xFF, 0xC5, 0xF9, 0xF9, 0xFF, 0xCA, 0xF5, + 0xEE, 0xFF, 0xCE, 0xF6, 0xEF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; diff --git a/crates/ironrdp-testsuite-core/tests/graphics/mod.rs b/crates/ironrdp-testsuite-core/tests/graphics/mod.rs new file mode 100644 index 00000000..50aa61f6 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/graphics/mod.rs @@ -0,0 +1,5 @@ +mod color_conversion; +mod dwt; +mod image_processing; +mod rle; +mod rlgr; diff --git a/crates/ironrdp-testsuite-core/tests/graphics/rle/mod.rs b/crates/ironrdp-testsuite-core/tests/graphics/rle/mod.rs new file mode 100644 index 00000000..6b1f155a --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/graphics/rle/mod.rs @@ -0,0 +1,57 @@ +use rstest::rstest; + +/// 64x64 tile samples were generated using rdp-rs crate +#[rstest] +#[case::x27019fd9f222cebce9dfebcddb12bfa0( + include_bytes!("../../../test_data/rle/tile-27019fd9f222cebce9dfebcddb12bfa0-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-27019fd9f222cebce9dfebcddb12bfa0-decompressed.bin"), +)] +#[case::x284f668a9366a95e45f15b6bf634a633( + include_bytes!("../../../test_data/rle/tile-284f668a9366a95e45f15b6bf634a633-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-284f668a9366a95e45f15b6bf634a633-decompressed.bin"), +)] +#[case::x28c08e75c82ab598c5ab85d1bfc00253( + include_bytes!("../../../test_data/rle/tile-28c08e75c82ab598c5ab85d1bfc00253-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-28c08e75c82ab598c5ab85d1bfc00253-decompressed.bin"), +)] +#[case::x2de3f3262a5eeecc3152552c178b782a( + include_bytes!("../../../test_data/rle/tile-2de3f3262a5eeecc3152552c178b782a-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-2de3f3262a5eeecc3152552c178b782a-decompressed.bin"), +)] +#[case::x3fc8124af9be2fe88b445db60c36eddc( + include_bytes!("../../../test_data/rle/tile-3fc8124af9be2fe88b445db60c36eddc-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-3fc8124af9be2fe88b445db60c36eddc-decompressed.bin"), +)] +#[case::x4d75aa6a18c435c6230ba739b802a861( + include_bytes!("../../../test_data/rle/tile-4d75aa6a18c435c6230ba739b802a861-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-4d75aa6a18c435c6230ba739b802a861-decompressed.bin"), +)] +#[case::x8b8ccc77526730d0cd8989901cc031ec( + include_bytes!("../../../test_data/rle/tile-8b8ccc77526730d0cd8989901cc031ec-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-8b8ccc77526730d0cd8989901cc031ec-decompressed.bin"), +)] +#[case::x94bb5b131eb3bc110905dfcb0f60da79( + include_bytes!("../../../test_data/rle/tile-94bb5b131eb3bc110905dfcb0f60da79-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-94bb5b131eb3bc110905dfcb0f60da79-decompressed.bin"), +)] +#[case::x9b06660a1da806d2d48ce3f46b45d571( + include_bytes!("../../../test_data/rle/tile-9b06660a1da806d2d48ce3f46b45d571-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-9b06660a1da806d2d48ce3f46b45d571-decompressed.bin"), +)] +#[case::xa412fbe2b435ac627ce39048aa3d3fb3( + include_bytes!("../../../test_data/rle/tile-a412fbe2b435ac627ce39048aa3d3fb3-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-a412fbe2b435ac627ce39048aa3d3fb3-decompressed.bin"), +)] +#[case::xaa326e7a536cc8a0420c44bdf4ef8d97( + include_bytes!("../../../test_data/rle/tile-aa326e7a536cc8a0420c44bdf4ef8d97-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-aa326e7a536cc8a0420c44bdf4ef8d97-decompressed.bin"), +)] +#[case::xfbcefc9af4db651aefd91bcabc8ea9fc( + include_bytes!("../../../test_data/rle/tile-fbcefc9af4db651aefd91bcabc8ea9fc-compressed.bin"), + include_bytes!("../../../test_data/rle/tile-fbcefc9af4db651aefd91bcabc8ea9fc-decompressed.bin"), +)] +fn decompress_bpp_16(#[case] src: &[u8], #[case] expected: &[u8]) { + let mut out = Vec::new(); + ironrdp_graphics::rle::decompress_16_bpp(src, &mut out, 64, 64).expect("decompress 16 bpp"); + assert_eq!(out, expected); +} diff --git a/crates/ironrdp-testsuite-core/tests/graphics/rlgr.rs b/crates/ironrdp-testsuite-core/tests/graphics/rlgr.rs new file mode 100644 index 00000000..9297ec25 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/graphics/rlgr.rs @@ -0,0 +1,665 @@ +use ironrdp_graphics::rlgr::*; +use ironrdp_pdu::codecs::rfx::EntropyAlgorithm; + +#[test] +fn encode_works_with_rlgr3() { + let input = [ + Y_DATA_DECODED.as_ref(), + CB_DATA_DECODED.as_ref(), + CR_DATA_DECODED.as_ref(), + ]; + let expected = [ + Y_DATA_ENCODED.as_ref(), + CB_DATA_ENCODED.as_ref(), + CR_DATA_ENCODED.as_ref(), + ]; + let mode = EntropyAlgorithm::Rlgr3; + + for (input, expected) in input.iter().zip(expected.iter()) { + let output_len = expected.len(); + let mut output = vec![0u8; output_len]; + encode(mode, input, &mut output).unwrap(); + assert_eq!(*expected, &output); + } +} + +#[test] +fn decode_works_with_rlgr3() { + let input = [ + Y_DATA_ENCODED.as_ref(), + CB_DATA_ENCODED.as_ref(), + CR_DATA_ENCODED.as_ref(), + ]; + let expected = [ + Y_DATA_DECODED.as_ref(), + CB_DATA_DECODED.as_ref(), + CR_DATA_DECODED.as_ref(), + ]; + let output_len = expected[0].len(); + let mode = EntropyAlgorithm::Rlgr3; + + let mut output = vec![0i16; expected.len() * output_len]; + for (i, (input, expected)) in input.iter().zip(expected.iter()).enumerate() { + decode(mode, input, &mut output[i * output_len..(i + 1) * output_len]).unwrap(); + assert_eq!(**expected, output[i * 4096..(i + 1) * 4096]); + } +} + +#[test] +fn decode_correctly_decodes_rl_without_leading_zeros_and_ones() { + let input = [0b1100_0000]; + let expected = [0, 1]; + let mode = EntropyAlgorithm::Rlgr3; + + let mut output = vec![0i16; expected.len()]; + decode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected.as_ref(), output.as_slice()); +} + +#[test] +fn decode_correctly_decodes_rl_with_not_null_sign_bit() { + let input = [0b1110_0000]; + let expected = [0, -1]; + let mode = EntropyAlgorithm::Rlgr3; + + let mut output = vec![0i16; expected.len()]; + decode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected.as_ref(), output.as_slice()); +} + +#[test] +fn decode_correctly_decodes_rl_with_leading_zeros() { + let input = [0b00000000, 0b10011001, 0b1100_0000]; + let expected = [[0; 66].as_ref(), [7].as_ref()].concat(); + let mode = EntropyAlgorithm::Rlgr3; + + let mut output = vec![0i16; expected.len()]; + decode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected, output); +} + +#[test] +fn encode_correctly_encodes_rl_with_leading_zeros() { + let expected = [0b00000000, 0b10011001, 0b1100_0000]; + let input = [[0; 66].as_ref(), [7].as_ref()].concat(); + let mode = EntropyAlgorithm::Rlgr3; + + let mut output = vec![0; expected.len()]; + encode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected, output.as_slice()); +} + +#[test] +fn decode_correctly_decodes_rl_with_leading_ones() { + let input = [0b11011111, 0b11111101]; + let expected = [0, 24]; + let mode = EntropyAlgorithm::Rlgr3; + + let mut output = vec![0i16; expected.len()]; + decode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected.as_ref(), output.as_slice()); +} + +#[test] +fn encode_correctly_encodes_rl_with_leading_ones() { + let expected = [0b11011111, 0b11111101]; + let input = [0, 24]; + let mode = EntropyAlgorithm::Rlgr3; + + let mut output = vec![0; expected.len()]; + encode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected.as_ref(), output.as_slice()); +} + +#[test] +fn decode_correctly_decodes_rlgr3() { + let input = [0b11000000]; + let expected = [0, 1, 0, 0]; + let mode = EntropyAlgorithm::Rlgr3; + + let mut output = vec![0i16; expected.len()]; + decode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected.as_ref(), output.as_slice()); +} + +#[test] +fn encode_correctly_encodes_rlgr3() { + let expected = [0b11000000]; + let input = [0, 1, 0, 0]; + let mode = EntropyAlgorithm::Rlgr3; + + let mut output = vec![0; expected.len()]; + encode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected.as_ref(), output.as_slice()); +} + +#[test] +fn decode_correctly_decodes_rlgr1() { + let input = [0b11000111, 0b11111000]; + let expected = [0, 1, 4, 0]; + let mode = EntropyAlgorithm::Rlgr1; + + let mut output = vec![0i16; expected.len()]; + decode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected.as_ref(), output.as_slice()); +} + +#[test] +fn encode_correctly_encodes_rlgr1() { + let expected = [0b11000111, 0b11111000]; + let input = [0, 1, 4, 0]; + let mode = EntropyAlgorithm::Rlgr1; + + let mut output = vec![0; expected.len()]; + encode(mode, input.as_ref(), output.as_mut_slice()).unwrap(); + assert_eq!(expected.as_ref(), output.as_slice()); +} + +const Y_DATA_ENCODED: [u8; 942] = [ + 0xc0, 0x01, 0x01, 0x15, 0x48, 0x99, 0xc7, 0x41, 0xa1, 0x12, 0x68, 0x11, 0xdc, 0x22, 0x29, 0x74, 0xef, 0xfd, 0x20, + 0x92, 0xe0, 0x4e, 0xa8, 0x69, 0x3b, 0xfd, 0x41, 0x83, 0xbf, 0x28, 0x53, 0x0c, 0x1f, 0xe2, 0x54, 0x0c, 0x77, 0x7c, + 0xa3, 0x05, 0x7c, 0x30, 0xd0, 0x9c, 0xe8, 0x09, 0x39, 0x1a, 0x5d, 0xff, 0xe2, 0x01, 0x22, 0x13, 0x80, 0x90, 0x87, + 0xd2, 0x9f, 0xfd, 0xfd, 0x50, 0x09, 0x0d, 0x24, 0xa0, 0x8f, 0xab, 0xfe, 0x3c, 0x04, 0x84, 0xc6, 0x9c, 0xde, 0xf8, + 0x80, 0xc3, 0x22, 0x50, 0xaf, 0x4c, 0x2a, 0x7f, 0xfe, 0xe0, 0x5c, 0xa9, 0x52, 0x8a, 0x06, 0x7d, 0x3d, 0x09, 0x03, + 0x65, 0xa3, 0xaf, 0xd2, 0x61, 0x1f, 0x72, 0x04, 0x50, 0x8d, 0x3e, 0x16, 0x4a, 0x3f, 0xff, 0xfd, 0x41, 0x42, 0x87, + 0x24, 0x37, 0x06, 0x17, 0x2e, 0x56, 0x05, 0x9c, 0x1c, 0xb3, 0x84, 0x6a, 0xff, 0xfb, 0x43, 0x8b, 0xa3, 0x7a, 0x32, + 0x43, 0x28, 0xe1, 0x1f, 0x50, 0x54, 0xfc, 0xca, 0xa5, 0xdf, 0xff, 0x08, 0x04, 0x48, 0x15, 0x61, 0xd9, 0x76, 0x43, + 0xf8, 0x2a, 0x07, 0xe9, 0x65, 0xf7, 0xc6, 0x89, 0x2d, 0x40, 0xa1, 0xc3, 0x35, 0x8d, 0xf5, 0xed, 0xf5, 0x91, 0xae, + 0x2f, 0xcc, 0x01, 0xce, 0x03, 0x48, 0xc0, 0x8d, 0x63, 0xf4, 0xfd, 0x50, 0x20, 0x2d, 0x0c, 0x9b, 0xb0, 0x8d, 0x13, + 0xc0, 0x8a, 0x09, 0x52, 0x1b, 0x02, 0x6e, 0x42, 0x3b, 0xd0, 0x13, 0x4e, 0x84, 0x01, 0x26, 0x88, 0x6a, 0x04, 0x84, + 0x34, 0x2a, 0xa5, 0x00, 0xba, 0x54, 0x48, 0x58, 0xea, 0x54, 0x02, 0xb4, 0x1d, 0xa7, 0xfa, 0x47, 0x82, 0xec, 0x7a, + 0x77, 0xfd, 0x00, 0x92, 0x66, 0x62, 0x04, 0xa6, 0x9b, 0xff, 0xf6, 0x80, 0xc0, 0x69, 0x01, 0xc2, 0x3e, 0x90, 0x14, + 0x20, 0x2f, 0xfc, 0x40, 0x96, 0x59, 0x58, 0x0c, 0xb1, 0x13, 0x68, 0x20, 0x2e, 0xb5, 0xf5, 0xdf, 0xff, 0xf8, 0xfc, + 0x56, 0x88, 0x60, 0x24, 0x53, 0xb5, 0x41, 0x46, 0x5f, 0xf8, 0xf1, 0x7e, 0xde, 0x4a, 0x08, 0x97, 0xe0, 0x55, 0x03, + 0x8f, 0xe5, 0x75, 0x61, 0x03, 0xf2, 0xe1, 0x90, 0x01, 0xa2, 0x8e, 0x88, 0x04, 0x98, 0x05, 0x93, 0x6b, 0xff, 0xea, + 0xc0, 0x60, 0xa1, 0x88, 0x04, 0x49, 0xbf, 0xf7, 0xff, 0x8c, 0xb4, 0x59, 0x90, 0x80, 0x30, 0x64, 0x53, 0xff, 0xf5, + 0xc4, 0x48, 0xda, 0xda, 0xcb, 0x80, 0x38, 0x61, 0x57, 0xb2, 0xaf, 0x00, 0xe8, 0x7b, 0x46, 0xe6, 0xd8, 0x02, 0x03, + 0x8a, 0x06, 0x18, 0x14, 0x32, 0x83, 0xd0, 0x8a, 0xee, 0xbc, 0x81, 0xb4, 0x28, 0xc4, 0x7f, 0xf9, 0xa1, 0x69, 0x00, + 0x91, 0xc5, 0x51, 0xff, 0xfe, 0x3f, 0xe9, 0xf1, 0x70, 0x30, 0x24, 0x10, 0xa7, 0xcb, 0x1f, 0x8a, 0x24, 0x93, 0xed, + 0x83, 0x00, 0x36, 0x20, 0xd1, 0x50, 0xe7, 0xd8, 0xad, 0x58, 0x20, 0x09, 0x22, 0x80, 0xd0, 0xca, 0x5d, 0x1a, 0xd7, + 0xf1, 0x60, 0x75, 0x2a, 0xf2, 0xd7, 0xf8, 0xc0, 0x32, 0x45, 0x86, 0x00, 0x43, 0x01, 0xfe, 0x80, 0xf7, 0x42, 0x81, + 0x74, 0x84, 0x4c, 0xa1, 0x60, 0x4c, 0xcb, 0x14, 0x58, 0x01, 0x4d, 0x18, 0xa1, 0xaa, 0x47, 0x0e, 0x11, 0x1a, 0x40, + 0x7d, 0x41, 0x02, 0xe3, 0x30, 0xcd, 0x33, 0x81, 0x34, 0x06, 0x46, 0x83, 0xa2, 0x47, 0x1c, 0x04, 0xaa, 0x20, 0x12, + 0xa2, 0x8b, 0x81, 0xc4, 0x9c, 0xa0, 0x2e, 0x06, 0x32, 0xf8, 0x86, 0x85, 0x01, 0xe8, 0x70, 0xf9, 0x46, 0x09, 0x6a, + 0xbf, 0xe0, 0xf5, 0xa4, 0xc8, 0x78, 0xe7, 0xd2, 0x97, 0x0b, 0xbc, 0x3c, 0x97, 0xff, 0xd5, 0x40, 0x94, 0xb2, 0xc1, + 0x18, 0x18, 0x11, 0x1f, 0x43, 0xc1, 0x18, 0xc3, 0x83, 0x7f, 0x9a, 0x31, 0xc4, 0x8e, 0x70, 0x56, 0xda, 0xf6, 0x17, + 0xde, 0xd1, 0x02, 0x0d, 0x42, 0x21, 0x13, 0xdc, 0x3a, 0x3c, 0x40, 0x9e, 0xf4, 0x01, 0x43, 0xea, 0x0c, 0x46, 0x73, + 0xa2, 0x7b, 0x0c, 0x80, 0xff, 0xe4, 0xad, 0x2e, 0x09, 0xb4, 0x63, 0xb0, 0x8c, 0x54, 0x59, 0xfa, 0xac, 0x76, 0x36, + 0x10, 0x05, 0xf0, 0x98, 0x88, 0x83, 0x42, 0x00, 0x20, 0x71, 0xcc, 0xc1, 0xa9, 0x97, 0x3e, 0x5a, 0x0d, 0x04, 0x50, + 0x92, 0x23, 0x20, 0x0d, 0x0a, 0x1c, 0x57, 0xd7, 0xff, 0x10, 0xf2, 0x03, 0x0f, 0x58, 0x1b, 0xa5, 0x11, 0xf8, 0xf1, + 0xb4, 0x12, 0xdb, 0x1a, 0x48, 0x56, 0x1f, 0xe3, 0xc7, 0x50, 0xe9, 0x16, 0xb4, 0xbc, 0xb0, 0x40, 0x93, 0xea, 0xb5, + 0x5b, 0x2f, 0xfc, 0x50, 0x0a, 0x6f, 0xcc, 0x25, 0xe0, 0x06, 0xab, 0x5f, 0x24, 0xfe, 0x8b, 0xcb, 0x42, 0x43, 0x7e, + 0x69, 0x02, 0x25, 0xc7, 0x38, 0x00, 0x6e, 0xe5, 0x80, 0xa8, 0xa4, 0x30, 0x44, 0x15, 0x8f, 0xe9, 0x0c, 0xd3, 0xa6, + 0xc2, 0x14, 0x34, 0x4a, 0xfe, 0x03, 0x7f, 0x06, 0xa5, 0x91, 0x02, 0x54, 0xf1, 0xa1, 0xa1, 0x53, 0xbf, 0x11, 0xf2, + 0x8f, 0x83, 0x67, 0x80, 0x09, 0x08, 0x12, 0x3f, 0xfd, 0x44, 0x91, 0xc2, 0x83, 0x30, 0x50, 0x07, 0x02, 0x82, 0x4d, + 0x31, 0x34, 0x06, 0x41, 0x79, 0x6f, 0xf0, 0xcc, 0x03, 0x79, 0x00, 0x2c, 0x05, 0x24, 0xec, 0x8d, 0x29, 0x15, 0xaf, + 0x44, 0xc8, 0xeb, 0x4f, 0xe1, 0xfd, 0xf1, 0x41, 0x48, 0x81, 0x08, 0xaf, 0xfe, 0x51, 0x48, 0xce, 0xe7, 0xf9, 0xb6, + 0x0a, 0x30, 0x83, 0x11, 0xf0, 0x0c, 0x3b, 0xd2, 0xa6, 0x24, 0x24, 0xef, 0x25, 0xfa, 0x5a, 0x3e, 0x92, 0x3e, 0x79, + 0x0e, 0x35, 0x61, 0xc8, 0xaa, 0x1c, 0x2e, 0x9a, 0x27, 0x7f, 0xff, 0xf0, 0x7d, 0x30, 0x5b, 0xbc, 0x91, 0xff, 0xfe, + 0x43, 0x24, 0x28, 0x66, 0xa7, 0x70, 0x99, 0x28, 0x6e, 0x2b, 0x18, 0x2b, 0xd4, 0xa1, 0x77, 0x3b, 0x96, 0x9f, 0xf7, + 0xeb, 0xbe, 0x1f, 0x04, 0x34, 0x75, 0x84, 0x31, 0x42, 0x4c, 0x65, 0xaa, 0x09, 0x50, 0xa0, 0xc4, 0x51, 0x31, 0xd3, + 0x26, 0x3a, 0x1b, 0xf4, 0x6e, 0x4a, 0x4e, 0x17, 0x25, 0x84, 0x78, 0x7d, 0x2c, 0x3f, 0x46, 0x18, 0xca, 0x5f, 0xf9, + 0xe5, 0x38, 0x2f, 0xd8, 0x71, 0x94, 0x94, 0xe2, 0xcc, 0xa3, 0x15, 0xb0, 0xda, 0xa9, 0xcb, 0x58, 0xe4, 0x18, 0x77, + 0x93, 0x8a, 0x51, 0xc6, 0x23, 0xc4, 0x4e, 0x6d, 0xd9, 0x14, 0x1e, 0x9b, 0x8d, 0xbc, 0xcb, 0x9d, 0xc4, 0x18, 0x05, + 0xf5, 0xa9, 0x29, 0xf8, 0x6d, 0x29, 0x38, 0xc7, 0x44, 0xe5, 0x3a, 0xcd, 0xba, 0x61, 0x98, 0x4a, 0x57, 0x02, 0x96, + 0x42, 0x02, 0xd9, 0x37, 0x11, 0xde, 0x2d, 0xd4, 0x3f, 0xfe, 0x61, 0xe7, 0x33, 0xd7, 0x89, 0x4a, 0xdd, 0xb0, 0x34, + 0x47, 0xf4, 0xdc, 0xad, 0xaa, 0xc9, 0x9d, 0x7e, 0x6d, 0x4b, 0xcc, 0xdc, 0x17, 0x89, 0x57, 0xfd, 0xbb, 0x37, 0x75, + 0x47, 0x5a, 0xec, 0x2c, 0x6e, 0x3c, 0x15, 0x92, 0x54, 0x64, 0x2c, 0xab, 0x9e, 0xab, 0x2b, 0xdd, 0x3c, 0x66, 0xa0, + 0x8f, 0x47, 0x5e, 0x93, 0x1a, 0x37, 0x16, 0xf4, 0x89, 0x23, 0x00, +]; + +const CB_DATA_ENCODED: [u8; 975] = [ + 0x00, 0xb0, 0x33, 0x56, 0xfa, 0x14, 0x1e, 0xff, 0x48, 0x7a, 0x7e, 0x0f, 0x10, 0x1f, 0xf4, 0x91, 0xc8, 0x10, 0x56, + 0x84, 0xff, 0x08, 0xec, 0xb4, 0xac, 0x0e, 0x0f, 0xff, 0xad, 0xc5, 0xe0, 0x1a, 0x2f, 0x82, 0x04, 0x9f, 0x91, 0xc2, + 0x0e, 0xfe, 0x48, 0x36, 0x79, 0x01, 0x42, 0x14, 0xff, 0xfe, 0x30, 0xf0, 0x08, 0x18, 0xf1, 0x81, 0x45, 0x9a, 0x60, + 0xc1, 0x79, 0xf0, 0x14, 0x12, 0x10, 0xce, 0xea, 0x31, 0x5a, 0xff, 0xfc, 0x20, 0x13, 0x82, 0x2f, 0xc9, 0x02, 0x1f, + 0x81, 0xcb, 0x00, 0xe1, 0x10, 0xd2, 0xb4, 0xbe, 0x87, 0xff, 0xb0, 0x1e, 0x27, 0x81, 0xb7, 0x04, 0x06, 0x3c, 0xc2, + 0x04, 0xf6, 0x06, 0x0e, 0x28, 0xbc, 0x40, 0xbf, 0x12, 0x1e, 0x86, 0xd4, 0x6a, 0x7f, 0x18, 0x1b, 0x96, 0x85, 0x4c, + 0x16, 0x80, 0xdf, 0x2c, 0xa5, 0x8d, 0x86, 0xa3, 0x4a, 0x8a, 0xb4, 0x1b, 0xa1, 0x38, 0xa9, 0xd5, 0xff, 0xff, 0xea, + 0x06, 0x20, 0xd2, 0x95, 0x1e, 0xf4, 0x2f, 0xb2, 0x12, 0x0e, 0x61, 0x78, 0x4a, 0x17, 0x52, 0x5d, 0xe4, 0x25, 0x1f, + 0xfe, 0xc0, 0xb3, 0x1f, 0xff, 0xff, 0xec, 0x02, 0x82, 0x80, 0x90, 0x41, 0x88, 0xde, 0x48, 0x2c, 0x42, 0x52, 0x0b, + 0x2f, 0x43, 0x7e, 0x50, 0x78, 0xf2, 0x67, 0x78, 0x41, 0x34, 0x3d, 0xc8, 0x0f, 0x67, 0xa1, 0xeb, 0x21, 0xfe, 0xc0, + 0x1f, 0x22, 0x60, 0x41, 0x6c, 0x00, 0x92, 0x4b, 0x60, 0x10, 0xd0, 0x0d, 0x01, 0x35, 0x05, 0x0e, 0x87, 0xa2, 0xa0, + 0x5d, 0x1f, 0xa3, 0xaf, 0x7f, 0xf1, 0xbe, 0x8f, 0xcd, 0xa5, 0x00, 0x1c, 0x10, 0x40, 0x15, 0x76, 0x81, 0x05, 0xef, + 0xee, 0x00, 0x60, 0x84, 0x00, 0x99, 0x40, 0x4a, 0x82, 0x17, 0xe9, 0xfc, 0xc4, 0x7f, 0xff, 0xfd, 0x04, 0x80, 0x06, + 0x06, 0xdc, 0xaf, 0xa7, 0x7e, 0x94, 0x75, 0x74, 0x01, 0x00, 0xe0, 0x91, 0x00, 0x85, 0x7f, 0x8e, 0xd6, 0x0b, 0x20, + 0x21, 0x30, 0xca, 0x62, 0x8e, 0x07, 0x04, 0xe9, 0x45, 0x40, 0x5f, 0x47, 0x4a, 0x30, 0x15, 0x41, 0xcb, 0xdf, 0xff, + 0xfc, 0xbf, 0xc3, 0xb4, 0x46, 0x6a, 0x01, 0x40, 0xd0, 0xa7, 0x34, 0x18, 0x24, 0x1c, 0x2a, 0x45, 0xfe, 0xa8, 0x05, + 0x08, 0x61, 0xfd, 0xa8, 0x80, 0x71, 0x01, 0x25, 0x9c, 0xc1, 0x47, 0x17, 0x37, 0x02, 0x7a, 0x15, 0xff, 0xf3, 0x01, + 0x45, 0x7f, 0xd6, 0x80, 0x60, 0x83, 0x67, 0xf8, 0x9d, 0x2f, 0xf4, 0xdd, 0x8c, 0x30, 0x01, 0x51, 0x42, 0xbc, 0x43, + 0x7a, 0x6b, 0x9f, 0x84, 0x1e, 0x00, 0x48, 0xc1, 0xe0, 0xb7, 0xe0, 0x7e, 0x99, 0xf2, 0x4a, 0xe9, 0x40, 0x02, 0x81, + 0xc3, 0x00, 0x24, 0x3a, 0xc5, 0x52, 0x0f, 0x91, 0xc8, 0x68, 0x25, 0x40, 0x99, 0xa4, 0x25, 0x1a, 0x04, 0xd0, 0xa2, + 0x91, 0xdd, 0xeb, 0x93, 0x00, 0x21, 0x49, 0x24, 0x8b, 0x40, 0x75, 0x38, 0x14, 0xa1, 0xfd, 0x3f, 0x88, 0x25, 0xbf, + 0x32, 0x00, 0xe3, 0x19, 0xfc, 0xb9, 0xf8, 0x6f, 0x81, 0xc0, 0x01, 0xb3, 0x93, 0x20, 0x09, 0x08, 0x25, 0x84, 0xe1, + 0x34, 0xd4, 0x1b, 0x48, 0x88, 0x11, 0xa0, 0x15, 0x59, 0xd7, 0x07, 0x81, 0x81, 0x3b, 0xa1, 0x40, 0x2e, 0x2f, 0x48, + 0x70, 0x09, 0xc4, 0x76, 0x49, 0x0f, 0x2e, 0x50, 0x2e, 0x46, 0x19, 0xa4, 0x16, 0xa2, 0x1b, 0x84, 0xa2, 0x89, 0x58, + 0xfc, 0x4f, 0x3f, 0x40, 0x90, 0x4c, 0xa3, 0x01, 0x32, 0x09, 0x02, 0x80, 0x9c, 0x91, 0x13, 0x2c, 0xba, 0xde, 0x5d, + 0x99, 0xf2, 0xff, 0xff, 0x3d, 0x5a, 0x1f, 0xa9, 0x02, 0x90, 0x8f, 0xf3, 0x08, 0xbd, 0x01, 0xf8, 0xd0, 0x2a, 0x95, + 0x41, 0x0c, 0x40, 0x0a, 0x20, 0xc4, 0xd4, 0xcc, 0x6b, 0x0f, 0xf0, 0x80, 0xb1, 0x5d, 0x28, 0x3d, 0x08, 0xc2, 0xf8, + 0x31, 0x02, 0x49, 0x88, 0x14, 0x28, 0xed, 0xe8, 0x86, 0x3b, 0x00, 0x9f, 0x95, 0x06, 0x37, 0x15, 0xa4, 0x59, 0xc8, + 0x80, 0xb6, 0x10, 0xf0, 0xe5, 0xb8, 0x18, 0x00, 0x56, 0x1c, 0xff, 0x95, 0x21, 0x0e, 0x7f, 0x2b, 0xc5, 0x08, 0x59, + 0x10, 0xe1, 0x46, 0x31, 0x8d, 0xec, 0xe0, 0xa1, 0x99, 0xbb, 0x21, 0xff, 0xfe, 0x30, 0x10, 0xd0, 0x05, 0xe3, 0x08, + 0x50, 0xfc, 0xf3, 0x0e, 0x00, 0x8d, 0x68, 0x8e, 0x07, 0xa6, 0x80, 0x34, 0x42, 0xed, 0x1f, 0x88, 0x00, 0xf0, 0x8a, + 0x21, 0xae, 0xf7, 0xfb, 0x80, 0x28, 0x86, 0x0f, 0xff, 0xff, 0x82, 0xea, 0x47, 0x95, 0x91, 0xe0, 0x04, 0x01, 0x44, + 0x0c, 0x29, 0xff, 0x0e, 0x33, 0xe8, 0xc0, 0x54, 0x04, 0x23, 0xfc, 0x81, 0x5b, 0xf0, 0x3c, 0x07, 0x10, 0x70, 0x30, + 0xd8, 0x21, 0x6f, 0xef, 0xde, 0x46, 0x09, 0x43, 0xfa, 0x5f, 0xff, 0x0d, 0x72, 0x30, 0xdd, 0x00, 0xdb, 0xe4, 0x48, + 0x24, 0x97, 0x08, 0x46, 0xb1, 0x49, 0xc4, 0x4d, 0x80, 0x12, 0x60, 0xff, 0xa4, 0xa6, 0xff, 0xf6, 0x8c, 0x00, 0x40, + 0x05, 0x02, 0xb4, 0x0f, 0xf0, 0x3e, 0xfc, 0x84, 0x38, 0x81, 0x94, 0x8b, 0xfe, 0x49, 0xef, 0xc0, 0x10, 0x49, 0x88, + 0x28, 0xa2, 0x1c, 0x2a, 0x8b, 0x64, 0xd4, 0x86, 0xd7, 0xff, 0xff, 0xff, 0xeb, 0x91, 0x6b, 0x11, 0x10, 0x00, 0x69, + 0x4c, 0xbf, 0xb4, 0x1c, 0xd8, 0x00, 0x07, 0x16, 0x80, 0x60, 0x0a, 0x1c, 0x82, 0x42, 0x27, 0x82, 0x43, 0xc9, 0x0a, + 0x64, 0x20, 0x5a, 0x5f, 0x4e, 0xbf, 0x8c, 0x38, 0x82, 0x36, 0x02, 0x07, 0x72, 0x79, 0x07, 0x23, 0xb4, 0xbb, 0x57, + 0x5f, 0xe8, 0x04, 0xdd, 0x39, 0xe9, 0x07, 0x95, 0xbe, 0x04, 0x2b, 0xdd, 0x8e, 0x22, 0xdc, 0x14, 0x2c, 0x61, 0xa3, + 0xa9, 0xcd, 0x4f, 0x82, 0x5d, 0xa0, 0x44, 0xdf, 0xf4, 0x96, 0xff, 0xf5, 0x2b, 0xff, 0xfe, 0x01, 0x19, 0xd2, 0xa2, + 0x9e, 0x43, 0xa5, 0x7f, 0xf0, 0x4c, 0x4c, 0x2b, 0x3c, 0x33, 0xe2, 0x55, 0xff, 0x04, 0x06, 0x29, 0x2c, 0x0d, 0x22, + 0x5d, 0x7c, 0x93, 0xba, 0x18, 0xaf, 0xf9, 0x32, 0xa6, 0xc3, 0x99, 0x46, 0x79, 0xe3, 0x06, 0xa6, 0x38, 0x8b, 0x92, + 0x22, 0x4b, 0xdb, 0x1b, 0x36, 0x20, 0xb0, 0x6c, 0x20, 0xce, 0x37, 0x42, 0xe1, 0x66, 0xd4, 0x49, 0x34, 0x42, 0x8b, + 0xfa, 0x9c, 0x12, 0x99, 0xdc, 0x06, 0x87, 0xfa, 0x46, 0xf8, 0x2f, 0x04, 0xa9, 0xd8, 0x82, 0x07, 0xa6, 0x30, 0x0f, + 0xc0, 0xdf, 0x35, 0xe8, 0x90, 0xf0, 0xff, 0xff, 0xa8, 0xe0, 0xd7, 0x02, 0x60, 0x1a, 0xc3, 0x20, 0x28, 0xa2, 0x31, + 0x29, 0x3c, 0xeb, 0x04, 0xa5, 0xdd, 0x48, 0x0e, 0x82, 0xa4, 0xb6, 0x56, 0x22, 0x06, 0x57, 0xe0, 0xda, 0x10, 0x27, + 0x31, 0x0e, 0x11, 0x77, 0xfe, 0x02, 0x60, 0x16, 0x48, 0x81, 0x8c, 0x0d, 0x05, 0x17, 0x7f, 0xcb, 0xbb, 0x7e, 0x25, + 0x2a, 0x41, 0xfd, 0x8a, 0x7f, 0xc9, 0x36, 0x7c, 0xe0, 0x98, 0x7e, 0x92, 0xef, 0x7e, 0x06, 0x03, 0x13, 0x3e, 0x20, + 0x3a, 0xbf, 0x4c, 0xc3, 0x0f, 0x2e, 0x80, 0x74, 0xbf, 0x39, 0x3c, 0xf0, 0xa6, 0xb2, 0xe9, 0x3f, 0x41, 0x55, 0x1f, + 0x2c, 0xf5, 0xd2, 0x7e, 0x8c, 0xae, 0x4e, 0xaa, 0x61, 0x3c, 0xbc, 0x3f, 0xc4, 0xc7, 0x36, 0xdc, 0x23, 0xc8, 0xb8, + 0x52, 0xe2, 0x8a, 0x80, /*0x18?*/ 0x00, 0x00, +]; +const CR_DATA_ENCODED: [u8; 915] = [ + 0x00, 0xb2, 0x46, 0xa2, 0x56, 0x0d, 0x12, 0x94, 0xaa, 0xbd, 0x01, 0x07, 0xff, 0xfa, 0x34, 0x0c, 0x5f, 0xf8, 0x0c, + 0x12, 0x50, 0xaf, 0xd6, 0xd1, 0x89, 0x40, 0xa4, 0xff, 0xe0, 0xce, 0xc4, 0x49, 0x25, 0x9d, 0xc1, 0xff, 0x7e, 0x60, + 0x24, 0x5d, 0xcc, 0x10, 0xc0, 0xbe, 0x5a, 0x12, 0xd3, 0xc3, 0xfe, 0x2d, 0x40, 0x7c, 0x28, 0x9e, 0x71, 0x01, 0xd2, + 0x6e, 0x86, 0x0b, 0xc8, 0xf2, 0x9b, 0x45, 0x08, 0x4c, 0x04, 0x52, 0x7e, 0xf2, 0x7e, 0xd9, 0xcc, 0x0b, 0x1c, 0x20, + 0x80, 0xae, 0xaf, 0xfe, 0xb0, 0x6d, 0x23, 0xf2, 0x41, 0xe3, 0x2e, 0x20, 0x11, 0x4b, 0x74, 0x89, 0xdd, 0xff, 0xa8, + 0x38, 0xa3, 0x95, 0x82, 0x15, 0xf0, 0xd0, 0xd5, 0xf1, 0x92, 0x8e, 0xee, 0xc0, 0x26, 0x81, 0xe9, 0x47, 0xff, 0xee, + 0x0d, 0x20, 0x34, 0x31, 0x3a, 0xef, 0x40, 0xb2, 0x29, 0x47, 0x19, 0x7f, 0x04, 0x27, 0xf1, 0x90, 0x85, 0x09, 0x86, + 0x7d, 0x42, 0xe2, 0x54, 0x5d, 0x5f, 0xe8, 0x0e, 0xd0, 0x2c, 0xaa, 0x16, 0xbf, 0x04, 0xa7, 0xf8, 0xa2, 0x46, 0x0b, + 0x08, 0x7a, 0x79, 0xe9, 0x28, 0x62, 0x7c, 0x33, 0xf4, 0x0b, 0x14, 0x82, 0xfa, 0x61, 0xeb, 0xc1, 0xff, 0x4c, 0xa4, + 0x11, 0x7f, 0x03, 0x68, 0x44, 0xc1, 0x1f, 0x81, 0x3a, 0x6c, 0x77, 0x95, 0x02, 0x2b, 0x53, 0x80, 0xe5, 0x10, 0x1e, + 0x90, 0xe8, 0xfd, 0x1f, 0xa6, 0x40, 0x0b, 0x13, 0xff, 0x4e, 0x4d, 0x7f, 0x52, 0xe8, 0xaf, 0x9a, 0xc1, 0x80, 0x0f, + 0x0a, 0x14, 0x02, 0x3c, 0xc0, 0x09, 0x13, 0xe7, 0xdc, 0xc0, 0x1a, 0x28, 0xa0, 0xe4, 0x83, 0x8e, 0x03, 0x88, 0xd5, + 0xaf, 0x1a, 0xbd, 0x91, 0x00, 0xb7, 0x4e, 0xba, 0xdf, 0xf8, 0xdb, 0xcc, 0x02, 0x43, 0xc4, 0x14, 0x2a, 0x3f, 0xc8, + 0x0d, 0x09, 0x1c, 0x44, 0xf4, 0x01, 0x3c, 0xca, 0x28, 0x56, 0x80, 0xa6, 0x85, 0x00, 0xea, 0x3e, 0x8f, 0xeb, 0x9f, + 0xfc, 0x6e, 0x07, 0xc4, 0xe0, 0x30, 0x78, 0xa0, 0x1e, 0x6f, 0x54, 0x78, 0x51, 0xff, 0x56, 0x4a, 0x01, 0x47, 0x02, + 0x4c, 0x21, 0x3b, 0xfb, 0x90, 0x0a, 0xcc, 0x1d, 0xd2, 0x47, 0xff, 0xfc, 0x70, 0x18, 0x22, 0xc0, 0xb9, 0x2f, 0xe9, + 0x7f, 0x91, 0xd3, 0x66, 0x2f, 0x80, 0x2c, 0x24, 0xa7, 0xfa, 0x84, 0x51, 0xab, 0x6b, 0x72, 0x00, 0xab, 0x33, 0x04, + 0xcf, 0x43, 0xff, 0x17, 0x51, 0x84, 0x0c, 0x01, 0x50, 0x10, 0x8f, 0x90, 0x34, 0x41, 0x44, 0x84, 0x8e, 0x08, 0x19, + 0x04, 0x48, 0x50, 0x84, 0x38, 0x3d, 0x02, 0x52, 0xf9, 0x7c, 0xd2, 0xd0, 0x1f, 0x13, 0x42, 0xa0, 0x21, 0x41, 0xc4, + 0x02, 0x02, 0x3d, 0x09, 0xc8, 0xfd, 0x60, 0x7d, 0x35, 0x4f, 0x7f, 0xff, 0xf9, 0x97, 0x6a, 0xd8, 0x00, 0xc3, 0x83, + 0x00, 0x09, 0x50, 0x4b, 0x90, 0x8a, 0xc7, 0x94, 0x4d, 0x47, 0xc1, 0x62, 0x32, 0x28, 0x24, 0x09, 0x52, 0x2e, 0x2e, + 0x1c, 0x96, 0x44, 0xa0, 0x09, 0xc8, 0xce, 0x64, 0xa9, 0x1c, 0x19, 0x0e, 0x52, 0x3e, 0x3e, 0x19, 0x93, 0xa0, 0x36, + 0x26, 0x22, 0x08, 0x9a, 0x00, 0xdd, 0x66, 0x3a, 0x93, 0xd5, 0x89, 0xd1, 0x40, 0x06, 0xd4, 0xa8, 0x22, 0x73, 0x7b, + 0x3d, 0x3f, 0xe3, 0x04, 0x94, 0xff, 0xff, 0xff, 0xff, 0x0c, 0x56, 0x77, 0xac, 0xe0, 0xc4, 0x06, 0x1f, 0xb8, 0xa5, + 0x80, 0xfd, 0x68, 0x1c, 0x32, 0x16, 0x03, 0xde, 0x71, 0x2a, 0x3d, 0x14, 0x19, 0xbe, 0xc2, 0x88, 0xd9, 0x24, 0x92, + 0x5f, 0xc5, 0x90, 0x0a, 0x85, 0xc2, 0x3f, 0x87, 0x03, 0xa8, 0x26, 0x17, 0xc4, 0x06, 0x86, 0x12, 0x87, 0x76, 0x0a, + 0x48, 0x16, 0xed, 0x96, 0x93, 0xec, 0x1b, 0x30, 0x73, 0xe8, 0x1a, 0x3f, 0xff, 0x4d, 0xce, 0x40, 0xf3, 0x0c, 0x51, + 0x4b, 0x84, 0x9e, 0x67, 0x2b, 0x15, 0x40, 0x1a, 0xa0, 0xfc, 0x10, 0x0f, 0xd8, 0x81, 0x35, 0x87, 0xff, 0x98, 0x0f, + 0x40, 0x00, 0xba, 0xc0, 0x71, 0xe2, 0x00, 0x18, 0x28, 0xb3, 0x82, 0xcc, 0x80, 0x6a, 0xa0, 0x43, 0xff, 0x2d, 0xd6, + 0x04, 0x8a, 0x68, 0xff, 0xff, 0xff, 0xfc, 0x1a, 0xf3, 0x1a, 0x2a, 0x06, 0xc0, 0x01, 0x40, 0x0c, 0x30, 0xc1, 0xd0, + 0xd7, 0x4f, 0xcb, 0x74, 0x1f, 0x07, 0xd3, 0xb4, 0x0d, 0x88, 0x98, 0xea, 0xda, 0x9f, 0xce, 0x2b, 0x3c, 0x55, 0xb3, + 0x40, 0x14, 0xff, 0xff, 0xff, 0xea, 0xdb, 0x9b, 0x92, 0xd8, 0x68, 0x08, 0x0b, 0x41, 0x09, 0x26, 0x40, 0x8c, 0xf1, + 0xb0, 0x9a, 0x98, 0xc0, 0x80, 0x8b, 0xf0, 0x3d, 0xe7, 0xec, 0x19, 0x68, 0x21, 0x03, 0x29, 0x7f, 0xe1, 0x6d, 0x4c, + 0x0f, 0x01, 0xd1, 0x51, 0x01, 0x1a, 0x50, 0x2a, 0x59, 0x27, 0x80, 0xc1, 0x6e, 0x33, 0xf1, 0x80, 0xe1, 0x49, 0x08, + 0xe9, 0x17, 0xff, 0xff, 0xff, 0x80, 0x5a, 0x10, 0x10, 0x36, 0x5e, 0xca, 0xf8, 0x3a, 0x00, 0x1e, 0xb0, 0x06, 0x84, + 0x01, 0xf3, 0x07, 0x1b, 0x4a, 0xc0, 0x1e, 0x21, 0x43, 0x8e, 0xa5, 0x55, 0x77, 0xc7, 0x65, 0x7c, 0xc2, 0xdf, 0x5e, + 0x0c, 0x42, 0x20, 0xd2, 0x48, 0x61, 0xc8, 0x1c, 0x65, 0xf8, 0xfe, 0x4c, 0x88, 0x71, 0x1f, 0x82, 0x50, 0x81, 0xa3, + 0x54, 0x09, 0x13, 0x28, 0x52, 0xf5, 0xe0, 0x82, 0xc3, 0x06, 0x7f, 0xfa, 0x2c, 0xcf, 0xf8, 0xf4, 0x7f, 0xff, 0xfd, + 0x01, 0x49, 0xa4, 0xb8, 0xde, 0x62, 0x84, 0xfe, 0xed, 0x65, 0x1f, 0x3c, 0x3c, 0xb2, 0x50, 0x76, 0x30, 0x5b, 0x03, + 0xc0, 0x08, 0xa6, 0x64, 0x90, 0xc8, 0xcd, 0x14, 0x6e, 0x69, 0x46, 0x7a, 0xc6, 0x1c, 0x87, 0xd7, 0x48, 0x7b, 0x49, + 0x05, 0x2d, 0x5e, 0x7f, 0xcb, 0x67, 0xf0, 0xd9, 0x0d, 0x1e, 0x9e, 0x53, 0xb7, 0x64, 0xa5, 0xa5, 0x10, 0x39, 0x06, + 0x11, 0x3f, 0xb1, 0xa9, 0xa6, 0xe8, 0x4d, 0x47, 0x77, 0xda, 0x43, 0x76, 0x89, 0x45, 0x09, 0x70, 0xc2, 0x38, 0x0f, + 0x09, 0x6f, 0xe7, 0x2d, 0x82, 0x35, 0x07, 0xfe, 0x64, 0x18, 0x2e, 0xb8, 0x04, 0x42, 0x54, 0x80, 0x43, 0x12, 0x6c, + 0x9a, 0x55, 0xc9, 0x0a, 0xa0, 0x79, 0x47, 0x52, 0x65, 0x2a, 0xff, 0x50, 0x11, 0xc9, 0x4e, 0xfe, 0x5b, 0x30, 0xa4, + 0xe8, 0x30, 0x63, 0xff, 0x21, 0x12, 0x1b, 0xdc, 0x1c, 0x01, 0x41, 0x51, 0x1f, 0xff, 0xfa, 0xc3, 0xe3, 0x55, 0xf1, + 0x66, 0xe2, 0xd5, 0x78, 0x5e, 0xfa, 0x4d, 0xf2, 0x61, 0x01, 0x26, 0x15, 0xa9, 0xf9, 0xd9, 0x32, 0x41, 0x90, 0x36, + 0x4e, 0xae, 0xe3, 0x0b, 0x16, 0x56, 0x8c, 0x6e, 0x42, 0x5d, 0xd8, 0x1e, 0xfe, 0x1d, 0x40, 0x3a, 0x50, 0x9f, 0x09, + 0x14, 0xeb, 0x6e, 0x48, 0x7a, 0x91, 0x88, 0x7b, 0x7d, 0x8f, 0x72, 0x42, 0x39, 0xb0, 0x1c, 0x65, 0x18, 0x23, 0x8b, + 0x60, /*0x30?*/ 0x00, 0x00, +]; + +const Y_DATA_DECODED: [i16; 4096] = [ + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, -2, 4, -5, -1, 0, 0, 0, 0, + 0, 0, -1, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 3, 0, 0, 0, 0, 0, 0, -1, 4, 4, -5, -1, 0, 0, 0, 0, 0, 0, -2, 5, + 0, 0, 0, 0, 0, 0, 2, 1, 0, 2, -5, -4, 1, 0, 0, 0, 0, 0, 0, 1, 3, -1, 5, 0, -1, 0, 0, 0, 0, 0, -2, -2, -3, 0, 0, 0, + 0, 0, 3, 0, 0, 7, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 2, -1, 0, 0, 0, 0, 0, -3, 1, 5, 0, 0, 0, 0, 0, 0, -1, + 0, 0, 1, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, -2, 0, 0, 0, 0, 0, 1, 0, -9, -3, 5, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 6, -2, -8, -1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 5, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, + 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, -1, 2, 0, 0, 1, 0, 0, -1, 0, -1, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -2, 0, 0, 0, + 0, 0, 3, -1, 0, -1, 0, 1, 0, 0, 0, 1, 2, -1, 0, -5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -4, 0, 0, 0, 0, 0, -1, -6, 1, + 0, -1, 0, 0, 0, 0, 0, 0, 0, 6, 0, -1, 0, 1, 0, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -5, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -1, -1, 1, 10, 0, 0, 0, -1, 1, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + -5, -5, 3, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, -1, -2, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 3, -1, -7, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -6, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 2, 4, 0, 0, 0, 0, 0, -1, + 0, 0, 0, 0, 6, -1, 0, 0, 0, 0, -1, 7, -1, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -3, -1, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 3, 1, 0, 0, 0, 0, 0, 4, 1, 2, -2, -2, 0, -1, 0, 0, 0, 0, 1, -1, -7, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, + 0, 0, -1, -10, 3, 1, 2, 0, -2, 1, 1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 3, -2, 0, 0, 0, 0, 0, 6, -8, + -2, 0, -2, 1, 0, 0, -1, -1, -1, 1, 1, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 3, 2, 0, 1, 0, 0, 1, + 1, 0, 0, 1, -2, 5, -3, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + 0, -3, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, + -1, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 1, 1, 1, -2, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -2, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, -1, 0, 0, -1, -1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, -2, -8, 2, 0, 0, 0, + 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, -2, 4, 4, -1, 0, 0, 0, 0, 0, 0, -1, 3, + -1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 3, 3, -7, -1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, -2, 2, -1, 0, 0, -1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, -5, -3, -4, -6, -6, 7, 1, 1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -2, -3, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 2, -1, 0, -2, -3, 1, 1, -8, 6, 0, -1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 6, -3, -1, 0, 0, 0, 0, 0, -1, + 5, 2, -6, -2, 0, 0, 0, 2, 7, -6, -1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 1, 6, 0, 0, 0, 0, 0, 0, -2, 1, 0, 0, + 0, 0, 0, 0, 0, -1, 6, -1, 1, -6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, -1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -1, -2, 0, -8, 6, 6, 4, 3, 3, 4, -1, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -1, -1, + 7, -8, 0, 1, -2, -4, -3, -1, 1, -3, -4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, -3, -5, 0, 1, 0, 0, 8, + -1, -7, -9, -7, -2, 6, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, -1, 1, 0, 0, 1, -1, 0, + 0, 0, 0, -1, 1, -1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 0, 1, + 1, 3, 3, 0, 1, 1, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 1, -4, -5, -8, + -6, -4, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 3, -9, -5, 10, 2, 2, 0, -1, 2, 6, -4, + -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 2, -1, -1, 1, 0, 0, 0, 0, 0, -1, 4, -2, -1, 1, + -1, 1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, -1, 1, 1, 0, 1, 0, -1, 3, + -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 1, 1, 1, 0, -2, -3, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -1, 1, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, + 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, + 0, 0, 0, 0, -1, 0, 0, 1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -1, -1, 2, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, + 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, -5, 3, -2, 1, 0, -1, 0, 0, 0, 0, 1, -4, -1, 0, + 0, 1, -5, -2, 1, 1, 1, 6, -2, -1, 0, 1, -4, 21, 6, 0, 0, -5, -1, 1, 2, 4, 4, 3, 5, 0, 0, -1, 4, -8, -11, 0, 0, -2, + 8, -1, 0, 3, 1, -18, 16, 1, -1, 2, 0, -4, -4, 0, 0, -6, 1, -3, -1, 1, 14, -23, 1, 0, 1, 0, 1, -3, -1, 0, 0, 0, -5, + -2, 3, -1, 11, -8, -3, 0, 1, 0, -1, -3, 1, 0, 14, 1, 0, -1, 0, 0, -15, -2, 7, 1, -1, 1, -3, -1, -1, -2, -14, 0, 1, + 0, 0, 5, -18, 14, -7, -1, -1, -2, 13, 1, -1, 3, -14, 1, -2, 2, -1, 11, -16, -2, 2, -1, 1, 0, -9, 0, -1, 19, 1, 1, + 1, 0, 2, 2, 8, 0, 0, 0, 0, 1, -13, 0, 0, -6, -3, -1, 0, -2, -2, 6, 13, 0, 0, 0, 1, 9, 1, 0, 0, -1, 2, 0, 0, 1, 0, + -5, 0, 0, -1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, -4, 1, 4, 4, -2, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + -4, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -1, 1, 1, -1, -3, 0, -2, -3, 0, 0, 0, 0, 0, + 0, 0, -2, -2, 0, 1, -1, 2, 0, 0, 0, 1, -1, 1, 0, 1, -8, -1, 0, 0, 1, -3, 0, 0, -1, 3, 0, 0, 1, -3, -3, -3, 3, -4, + 0, 0, 0, 1, 0, 0, 3, -3, 3, -1, 1, 1, 0, 1, -5, 20, 0, 0, 0, -1, 2, 7, -1, 0, 3, -2, 1, 0, 4, -1, 0, 1, 0, 1, -3, + 2, -9, -12, -19, 10, -1, 2, 0, -1, 1, 9, -13, 0, 0, 1, -4, -13, 0, 0, 16, -7, 0, 16, -3, 1, -2, -1, 2, -1, 0, 0, 0, + 1, 1, 0, 0, 4, -3, -17, -14, 2, -4, -14, -1, 0, -1, 1, 0, -1, 0, 1, 1, -2, 1, 3, 17, 13, 15, 9, -2, 0, 2, -3, 2, 2, + -2, 2, 1, -1, 1, 0, 0, -2, 5, 7, -1, 0, 1, 4, -7, -14, -7, 2, -2, 0, 0, 0, 0, 0, 0, -1, 1, 0, -3, 17, 8, -1, 4, 8, + -9, 6, 0, 1, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, -2, -2, 2, 0, 0, 5, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -5, -4, + -2, -6, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -4, -1, 0, 0, 0, 0, + 0, 0, -1, -1, 1, -1, 0, 0, 0, 0, 2, 2, 0, 0, 0, 3, 0, 0, 1, 1, -1, 1, 0, 0, 0, 0, -4, -8, 0, 0, 0, -1, 0, 0, -1, + -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, -1, -3, 3, 0, 0, 0, -1, 1, 0, 0, 0, 0, 1, -2, 0, 0, -1, 3, 1, 2, -1, 0, + 0, 0, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 2, 0, -1, 1, 0, 0, 0, + 1, 1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, -2, 0, 0, 2, 2, 1, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 2, 0, 0, + 1, -2, 1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -2, -2, 1, 0, -1, -2, 0, -1, 8, 6, 3, + 0, -1, -6, 0, 4, 5, -13, -53, -3, -2, 0, -2, -15, -7, 25, 8, 3, 7, -25, 26, -12, 5, 20, 6, 7, -5, -13, 2, -2, 5, + -54, 10, -2, 30, 3, -46, 11, -9, -1, -2, 1, -18, -2, 8, -3, 3, -5, -4, -1, -5, -2, -3, 0, 1, -10, -3, 1, -1, 1, -5, + -1, -3, 3, 7, -14, -13, 10, 0, -1, 11, 2, 5, 2, 12, 0, -8, 24, -21, -49, -4, 10, 9, 31, -1, 5, 10, 1, 13, -57, -52, + 9, 12, -6, -18, 5, 1, -3, -4, -1, 13, 21, 8, 11, 3, 7, 7, -2, -3, -1, 0, 2, -14, -19, -7, 1, -1, -1, -3, -2, 0, 0, + -1, -1, -1, -7, 3, 4, 7, -1, -6, -14, -1, -7, 3, -12, -16, -2, 2, -1, -9, 1, 7, -21, 25, -4, -2, -23, 2, 2, 3, -4, + 22, -4, 5, -4, -2, -2, 4, -7, 0, 0, 3, 2, 17, -6, 7, 5, 3, 1, -5, 0, 1, 0, 0, 6, 1, -4, 2, 0, -1, 1, -1, -14, 3, 0, + -7, -4, 50, -13, -11, 34, -26, -23, -3, -1, 45, -4, 21, 57, -17, -43, -17, -15, 27, -7, 25, 30, -23, 65, -13, -88, + 45, -103, 85, 71, -116, 88, -30, -57, 57, -101, 94, 25, 17, 5, 6, -107, 15, -31, 20, -26, 142, -18, -23, -95, 69, + -7, -13, -9, 5, 18, -38, -18, +]; + +const CB_DATA_DECODED: [i16; 4096] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, + 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 7, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 3, 2, 2, 0, 0, 0, 0, 0, 0, 0, + 1, -1, 1, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -2, -1, -1, 1, 0, 1, 0, 0, 0, 3, -7, 7, -8, 1, + 0, 0, 0, 0, 0, 5, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 6, 0, -10, 0, 0, 0, -1, 0, 1, 0, 0, 0, -2, -5, -2, 0, 0, 0, 0, + 0, 5, 4, 0, 0, 0, 0, 0, 0, 1, 1, 0, -2, 6, 9, -2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 6, 0, 0, 0, 0, 0, -1, -6, -2, -2, + 0, 0, 0, 0, 0, 1, 0, 0, -7, 0, 5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 2, -6, 0, 4, 0, 0, 0, 0, 0, + 1, -1, 1, 0, -1, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, -2, 0, 0, 0, 0, 1, 1, 0, 6, 3, + -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, -1, 0, 0, 0, 0, 0, 0, -4, 1, 6, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -1, 9, 0, 0, 0, 0, 0, 0, -8, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, -3, -1, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -8, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, -4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, + 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 8, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -1, 1, 0, 0, -1, 0, -1, 0, 1, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, -1, 0, 0, 0, 1, -2, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 3, 2, + -1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 1, 4, 0, 0, 0, 0, 0, 2, + 0, 0, 0, -1, -1, 10, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -2, 0, 0, 0, 0, 0, -2, 2, 0, 0, 0, + -10, 1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, -4, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, -3, -1, 0, 0, + 0, 0, 0, -1, 0, -1, 1, 1, 0, 0, 0, 0, 0, 0, -1, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 12, 0, 0, 0, 0, 0, 0, 1, + -1, 0, -1, 0, 1, 0, -1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 5, 0, 0, 0, 0, 0, -1, 1, 1, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, -1, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, + 0, -1, 5, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -2, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 3, 0, 1, 0, 0, 0, 0, 1, -2, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 2, 4, 5, 2, -1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 7, 4, -8, -7, -4, -4, -6, -7, 7, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, -3, -3, 1, 0, 0, 0, 0, 0, 0, 1, -8, 6, -1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 1, -6, 8, -1, 0, 0, 0, 0, 0, 2, 8, 2, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -8, 7, 7, 4, 5, 8, 1, -1, -7, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, -1, 1, 0, 0, 0, 0, 1, -2, -4, -4, -1, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -1, 2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -3, -1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, + 0, 0, 0, 0, 1, -4, -6, 4, 7, 5, 4, 6, 5, -5, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -1, -4, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 5, -1, 0, 1, 1, 1, 2, 0, -1, 5, -4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 5, -3, 2, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, + 0, 0, -1, -3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, -1, -1, 1, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, -3, 1, -1, 3, -4, 0, 0, 0, 0, 0, 0, 0, 1, -2, -7, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 0, 5, -5, -4, -3, -3, -7, -9, 3, 8, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, -4, 4, 0, -1, + 1, 3, 4, 2, -2, 4, 8, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, -1, 0, 0, -4, 0, 5, 6, 7, 2, -9, + -7, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, -2, 4, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -5, 2, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 1, 1, -2, 0, -1, 0, 1, -1, -1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 8, -7, 0, 0, 0, 0, 0, 0, 1, -2, -11, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 5, 0, -9, -10, -7, -4, 7, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 1, 0, + 0, 0, 2, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 2, 0, -1, -2, 1, 1, -2, 0, + 0, 0, 0, 0, 0, 1, 1, -2, -8, -1, 1, 0, -1, -5, 9, 2, 0, 0, -1, 2, 0, 0, 0, 1, -17, 0, 0, -1, -1, -18, 20, -1, 1, 0, + 5, -8, 8, 0, 0, 17, 1, 0, 0, 2, 3, -6, -10, 3, -1, 1, 0, 3, 32, 0, -1, 5, 5, -1, 0, 5, 4, 14, -16, 1, 0, 0, -1, + -17, 2, 0, -1, -17, -3, -2, 1, -1, -9, 15, -1, 0, 1, -1, -1, -8, -4, 0, 1, 0, 3, 1, -1, 1, -6, 3, 2, 0, 2, -1, 2, + 11, 0, 0, -3, 0, 1, 1, 0, 0, 7, 1, -3, 0, -1, -4, 0, 4, 0, 0, 3, 0, 0, 0, 0, -2, 10, -7, 4, 0, -1, 3, -24, -2, 0, + -1, 4, 0, 2, -1, 0, -5, 10, 2, -1, -3, 4, -1, 11, -1, 0, -4, 1, 0, -1, 0, 0, -2, -6, 0, 0, 1, -2, 0, 24, 0, 0, 1, + 2, 1, 0, 0, 0, -1, -17, 0, 0, 0, -1, -26, 0, 0, 0, 0, -1, 0, 0, 0, 0, 22, -2, -1, 0, 0, 2, -7, -3, 0, 0, 0, 0, 0, + 0, 0, 0, -1, 2, 1, -1, -2, -4, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 10, + 6, -2, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, -3, -19, -9, -13, -13, 14, -2, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, -1, 0, 0, 0, + -2, 14, 2, -2, -2, 6, 8, 0, 0, 1, -2, 0, 0, 0, 1, 0, 1, 0, -17, -6, 1, -8, -9, 0, 0, 1, -1, 0, 1, 1, -1, 0, 0, 0, + -2, -1, 0, 0, -2, 0, 0, 1, 1, -1, -8, 0, 3, -1, 0, 1, 1, 3, 1, 0, 0, 0, 1, -7, 13, 13, 12, 14, -7, 0, 0, 0, -1, 1, + 9, -7, 2, 0, 0, -1, 2, 0, 0, -8, 4, 0, -9, 2, 2, 0, 1, -12, 3, 0, 0, 0, 0, 0, 0, 0, -2, 1, 9, 10, -4, 2, 35, 4, 0, + 0, 0, 0, 1, 0, 0, -1, 1, 0, -1, -10, -8, -21, -13, 3, 0, -1, 1, -1, 0, 1, -1, 0, 1, 0, 1, 1, -5, 14, 14, -2, 0, 0, + 0, 2, 3, 2, 0, 1, 1, 0, 1, 0, 0, 1, 1, -1, 0, 0, -3, -1, 0, 0, -2, 2, 0, -1, -1, 0, 0, -1, 1, 0, 0, 0, -1, 0, 0, 0, + 0, 2, -12, -4, 1, 1, 2, -20, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 19, 16, 15, 24, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 6, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -1, 0, 0, -1, 3, -2, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 3, 0, 0, -1, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, -1, 2, 2, -1, 1, -1, 3, 4, 0, + 0, 1, 2, 0, 0, 0, 1, 2, -1, 0, 0, 0, 0, 1, 0, 0, 0, -2, -1, -1, 0, 0, 1, 1, 0, 0, 0, 0, 0, -1, 0, 0, 1, -2, 0, 0, + 0, 2, -2, 0, 0, 0, -1, 1, -1, 4, 0, 0, 0, 0, 0, 0, 0, -2, 0, -1, 0, -1, -1, 1, -1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -1, 0, 0, -1, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, -2, + 0, -2, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, -3, 0, 0, 0, 2, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -9, -1, -2, 5, 1, 1, 0, 1, 33, -2, -2, 5, 1, 2, -3, -4, -14, -3, 2, 76, 4, 5, 17, + 3, -37, 1, -15, -8, 3, -1, 80, -5, 14, -2, -11, 2, 7, -16, 15, -2, 3, -2, 29, -2, 1, -51, -8, 6, -4, 3, 4, 6, 4, + 35, -1, -1, 1, -2, 8, -3, -7, 17, 1, 1, -4, 35, -4, 34, -5, 2, -1, 3, 0, 0, 2, 3, -17, 46, 10, -2, 4, -9, -5, -2, + 3, 4, 0, 1, 4, 28, 33, 2, -3, 14, -6, -1, 0, -4, -1, -7, 38, 60, -5, -3, 2, 6, 1, 4, 4, -3, 1, -1, -3, -2, -7, -28, + -5, -31, 8, 2, 0, 0, -8, 56, 85, 24, -3, 0, -10, 3, -4, 4, 2, 1, 0, 1, 4, 2, 4, 15, 10, 6, -9, -1, -1, -4, 1, 5, 2, + 0, 4, 1, 13, -3, 10, -11, -4, 2, 7, -1, 0, -2, 1, -14, -3, 2, 2, 0, -1, -1, 3, 0, -5, -9, -4, -2, 2, -5, -28, 1, 7, + 17, 1, 0, 0, 0, -18, 5, 15, -6, 0, 55, -4, 34, -14, 18, -33, 7, -1, -16, 19, -109, -29, 19, 123, 10, -6, -31, 13, + -129, 32, 31, -43, 7, 9, 83, -29, -34, 15, 32, -44, -12, -18, 79, 31, -21, -40, 58, -51, -33, 63, -8, 35, -31, -13, + -10, -20, -44, 102, -11, 9, 2, 17, -136, 4, -58, 208, -49, 6, 4, 11, -83, -38, 84, 54, +]; + +const CR_DATA_DECODED: [i16; 4096] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, + 0, 1, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1, 0, 2, + -1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, -13, 2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -10, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, -4, -1, -1, 0, 0, 0, 0, 0, + 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 3, -3, 3, + -1, 0, 0, 0, 0, 0, -7, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -6, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, + 0, -7, -4, 0, 0, 0, 0, 0, 0, -2, -1, 0, 2, -6, -4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 1, 9, 1, 2, + 0, 0, 0, 0, 0, -2, 0, 0, 6, 0, -3, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 9, -1, -3, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -6, + -3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 4, -1, -6, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 13, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 3, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 4, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, -1, 2, 0, 0, 0, 0, 0, -1, 0, -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, + 0, 0, 0, -1, 0, 0, -1, 0, 1, 0, 0, 0, 0, 1, -1, 0, -3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -5, 0, 0, 0, 0, 0, 0, 2, + 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 4, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -1, -1, 1, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + -3, -3, 2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 2, 0, -5, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, -7, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 6, -1, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -2, -1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, + 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -8, 0, 0, 0, 0, 0, + 1, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, -4, 0, 0, 0, 0, 0, -1, 2, 0, 0, -1, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, -4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -2, 0, + 0, 0, 1, 1, 0, 0, 0, 0, 1, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 1, 0, -1, 0, 1, 0, + 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -1, 0, 0, 1, 0, -1, 1, 1, -1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -3, -6, -6, -3, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -10, -5, 11, 9, 6, 5, 9, 11, -11, -1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 4, 4, -1, 0, 0, 0, 0, 0, 0, -1, 11, -10, 2, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, 0, 3, -4, 0, 0, 0, 0, 0, 0, -1, -4, -1, -1, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -3, -3, -2, -2, -4, -1, 1, 3, 0, 0, 0, 0, 0, 0, + 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, -1, 0, 0, 0, 0, -1, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 2, 1, -1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 6, 8, -5, -7, -5, -4, -5, -4, 5, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -2, -8, 1, 0, -1, 0, -1, -2, 0, 1, -6, 4, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, + 0, 0, -1, 0, 1, 0, 0, 0, 0, 1, 4, -4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 4, -1, 1, -4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -1, -1, 0, -5, 4, 4, 3, 3, 4, 4, -1, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, -1, + 4, -5, 0, 1, -1, -3, -3, -1, 1, -3, -5, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -3, 0, 1, 0, 0, 5, + 0, -5, -6, -5, -1, 6, 4, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, -1, 0, + 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 1, 1, + 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, -1, 2, 1, -2, -2, 1, 0, -1, 0, -1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 2, 0, 0, 0, 0, 0, 0, 0, 1, 7, -2, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, -1, 5, 6, 4, 3, -5, -6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, -1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -4, + 0, 1, 2, 0, -2, 3, 0, 0, 0, 0, 0, 0, -1, -1, 3, 13, -2, 1, -2, 2, 7, -15, -1, 0, 0, 1, -1, 0, 0, 0, -3, 25, 2, -1, + 0, 0, 20, -7, 1, -1, 0, -2, 6, -3, 0, 0, -20, 0, -1, -1, -2, -4, 5, 5, -1, 0, -1, 1, -3, -17, 0, 1, -5, -7, 2, 0, + -3, -3, -13, 12, 0, 0, 1, 0, 7, -2, 0, 1, 28, 0, 1, -1, 2, 10, -16, 1, 0, 1, -1, 0, 3, 1, 0, -1, 0, -3, -2, 2, 0, + 7, -5, -2, 0, 0, 0, -1, -6, 0, 0, -2, 1, -3, -1, -1, 0, -10, -2, 4, 0, 0, 1, -1, -2, 0, 0, 3, -1, 0, 0, 1, 3, -12, + 9, -5, 0, 0, -2, 16, 1, 0, -1, 2, 0, -4, 2, 0, 7, -11, -2, 1, 0, -1, 0, -7, 0, 0, -3, -1, 0, 1, -1, 0, 2, 6, 0, 0, + 0, 0, 0, -16, 0, 0, 1, -1, -1, -1, 1, 0, 1, 12, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 1, 0, 0, 0, -2, 4, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, -1, 3, 1, 3, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -1, 0, 0, 0, 0, 0, 0, + 0, 0, 2, 0, -13, -8, 3, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, -3, 3, 27, 12, 19, 20, -21, 3, 0, 0, 0, 0, 0, -1, -1, 0, + -1, 0, -1, 0, 0, 0, 0, -5, -1, 1, 1, -3, -4, 0, 0, -1, 5, -1, 0, 0, -2, 0, -1, -2, 7, 2, 0, 5, 3, 0, 0, -1, 0, -1, + 0, -2, 1, -1, -1, 1, 1, -1, 0, -2, 7, 0, 0, -1, -1, 3, 5, 1, -2, 2, -1, 0, 0, 1, 0, 0, 0, 0, -1, 11, -17, -14, -10, + -14, 7, -1, 1, 0, 0, 0, 2, -3, 0, 0, 0, 3, 2, 0, 0, 9, -5, 0, 11, -2, -1, -1, -2, 5, -1, 0, 0, 0, 0, 1, 0, 0, 3, + -2, -11, -10, 2, -2, -18, -3, 0, 0, 0, -1, -2, 0, 0, 0, -2, 1, 2, 11, 9, 12, 9, -2, 0, 0, 0, 1, -1, -3, 2, 0, -1, + 0, 0, 0, 1, -3, -3, 0, 0, 0, -1, 1, 2, 1, -4, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -4, -2, -1, -1, 0, 3, 1, 0, 0, 0, + 0, 0, -1, 0, 0, -1, 1, 0, 0, 0, 0, 0, 4, 3, 0, -1, 0, 14, -3, 0, 0, -1, 0, 0, 0, 0, 0, 0, 2, -11, -11, -9, -15, -2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -6, 1, 0, -1, 2, -4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, 0, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, 0, + 0, 0, -1, -1, 0, 0, 0, -1, -2, 0, 0, -2, -2, 0, 0, 0, -1, -2, 1, 0, 0, 0, 0, -2, -3, 0, 0, 3, 1, 1, 0, 0, -1, -1, + 0, 0, 0, 0, 0, 0, 0, 0, -1, 3, 0, 1, -1, -2, 2, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 1, -1, 0, -1, 2, 1, 2, -1, 0, 0, 0, + -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, -1, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, -1, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 13, 4, 5, -8, -2, 0, 1, -1, -43, -7, -3, -4, 1, -1, + 1, 5, 22, -1, -1, -54, -2, -2, -4, -4, 52, -5, 17, 7, 1, -3, -33, -4, -25, 1, 15, 3, 2, 2, -11, 4, -14, 5, -34, 6, + -1, 35, 6, 11, 4, -2, -5, -4, -7, -21, 1, -2, 0, 1, -6, 1, 3, -11, 0, -2, 6, -45, 14, -45, 4, -1, 1, -3, 1, 1, -7, + -7, 5, -19, -4, 2, -4, 8, 4, -1, 3, 0, 0, 2, -15, -27, -36, -2, 5, -1, 14, 2, -5, 7, 0, 8, -39, -41, 6, 1, 4, -12, + -6, 0, -2, -2, -1, -4, -5, -3, 6, 14, -2, 22, -5, -2, -1, 0, 5, -33, -51, -15, 2, 0, 14, -1, 8, -5, -1, 0, 0, -1, + -2, -5, -5, -10, -5, -3, 2, 2, 5, 5, -4, -8, -1, 0, -3, 2, -13, 5, -14, 15, -1, -1, -12, 1, -1, 2, -3, 15, -1, 3, + -3, 1, 5, -4, -2, 0, 2, 6, 2, -4, 0, 2, 15, 0, -5, -12, 0, 0, 0, 0, 12, -3, -10, 4, 0, -68, 7, -44, 27, -28, 42, + -3, 3, 1, -23, 147, 12, 8, -162, 6, 3, 10, -12, 147, -60, -13, -24, -1, 1, -44, 40, 33, -24, -22, 48, -14, -22, + -38, -3, -1, 61, -68, 60, -4, -37, 1, -18, 2, 29, 29, 8, 18, -74, 10, 5, -17, -22, 108, -16, 33, -124, 22, -7, 0, + -4, 48, 26, -46, -33, +]; diff --git a/crates/ironrdp-testsuite-core/tests/input/fastpath_packets.rs b/crates/ironrdp-testsuite-core/tests/input/fastpath_packets.rs new file mode 100644 index 00000000..23b8e0f0 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/input/fastpath_packets.rs @@ -0,0 +1,330 @@ +use ironrdp_input::*; +use ironrdp_pdu::input::fast_path::{FastPathInputEvent, KeyboardFlags, SynchronizeFlags}; +use ironrdp_pdu::input::mouse::PointerFlags; +use ironrdp_pdu::input::mouse_x::PointerXFlags; +use ironrdp_pdu::input::{MousePdu, MouseXPdu}; +use rstest::rstest; + +enum MouseFlags { + Button(PointerFlags), + Pointer(PointerXFlags), +} + +#[rstest] +#[case::left(MouseButton::Left, MouseFlags::Button(PointerFlags::LEFT_BUTTON))] +#[case::middle(MouseButton::Middle, MouseFlags::Button(PointerFlags::MIDDLE_BUTTON_OR_WHEEL))] +#[case::right(MouseButton::Right, MouseFlags::Button(PointerFlags::RIGHT_BUTTON))] +#[case::x1(MouseButton::X1, MouseFlags::Pointer(PointerXFlags::BUTTON1))] +#[case::x2(MouseButton::X2, MouseFlags::Pointer(PointerXFlags::BUTTON2))] +fn mouse_buttons(#[case] button: MouseButton, #[case] expected_flag: MouseFlags) { + let mut db = Database::default(); + + { + let packets = db.apply(core::iter::once(Operation::MouseButtonPressed(button))); + let packet = packets.into_iter().next().expect("one input event"); + + let expected_input_event = match expected_flag { + MouseFlags::Button(flags) => FastPathInputEvent::MouseEvent(MousePdu { + flags: flags | PointerFlags::DOWN, + number_of_wheel_rotation_units: 0, + x_position: 0, + y_position: 0, + }), + MouseFlags::Pointer(flags) => FastPathInputEvent::MouseEventEx(MouseXPdu { + flags: flags | PointerXFlags::DOWN, + x_position: 0, + y_position: 0, + }), + }; + + assert_eq!(packet, expected_input_event); + } + + { + let packets = db.apply(core::iter::once(Operation::MouseButtonReleased(button))); + let packet = packets.into_iter().next().expect("one input event"); + + let expected_input_event = match expected_flag { + MouseFlags::Button(flags) => FastPathInputEvent::MouseEvent(MousePdu { + flags, + number_of_wheel_rotation_units: 0, + x_position: 0, + y_position: 0, + }), + MouseFlags::Pointer(flags) => FastPathInputEvent::MouseEventEx(MouseXPdu { + flags, + x_position: 0, + y_position: 0, + }), + }; + + assert_eq!(packet, expected_input_event); + } +} + +#[test] +fn keyboard() { + let mut db = Database::default(); + + { + let to_press = [ + Operation::KeyPressed(Scancode::from_u8(false, 0)), + Operation::KeyPressed(Scancode::from_u8(false, 23)), + Operation::KeyPressed(Scancode::from_u8(false, 39)), + Operation::KeyPressed(Scancode::from_u8(true, 19)), + Operation::KeyPressed(Scancode::from_u8(true, 20)), + Operation::KeyPressed(Scancode::from_u8(false, 90)), + ]; + + let expected_inputs = [ + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 0), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 23), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 39), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::EXTENDED, 19), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::EXTENDED, 20), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 90), + ]; + + let mut expected_keyboard_state = KeyboardState::ZERO; + expected_keyboard_state.set(0, true); + expected_keyboard_state.set(23, true); + expected_keyboard_state.set(39, true); + expected_keyboard_state.set(256 + 19, true); + expected_keyboard_state.set(256 + 20, true); + expected_keyboard_state.set(90, true); + + let actual_inputs = db.apply(to_press); + let actual_keyboard_state = db.keyboard_state(); + + assert_eq!(actual_inputs.as_slice(), expected_inputs.as_slice()); + assert_eq!(*actual_keyboard_state, expected_keyboard_state); + } + + { + let to_press = [ + Operation::KeyReleased(Scancode::from_u8(false, 0)), + Operation::KeyReleased(Scancode::from_u8(false, 2)), + Operation::KeyReleased(Scancode::from_u8(false, 3)), + Operation::KeyReleased(Scancode::from_u8(true, 19)), + Operation::KeyReleased(Scancode::from_u8(true, 20)), + Operation::KeyReleased(Scancode::from_u8(false, 100)), + ]; + + let expected_inputs = [ + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 0), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE | KeyboardFlags::EXTENDED, 19), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE | KeyboardFlags::EXTENDED, 20), + ]; + + let mut expected_keyboard_state = KeyboardState::ZERO; + expected_keyboard_state.set(23, true); + expected_keyboard_state.set(39, true); + expected_keyboard_state.set(90, true); + + let actual_inputs = db.apply(to_press); + let actual_keyboard_state = db.keyboard_state(); + + assert_eq!(actual_inputs.as_slice(), expected_inputs.as_slice()); + assert_eq!(*actual_keyboard_state, expected_keyboard_state); + } +} + +#[test] +fn keyboard_repeat() { + let mut db = Database::default(); + + let to_press = [ + Operation::KeyPressed(Scancode::from_u8(false, 0)), + Operation::KeyPressed(Scancode::from_u8(false, 0)), + Operation::KeyPressed(Scancode::from_u8(false, 0)), + Operation::KeyPressed(Scancode::from_u8(false, 20)), + Operation::KeyPressed(Scancode::from_u8(false, 90)), + Operation::KeyPressed(Scancode::from_u8(false, 90)), + Operation::KeyReleased(Scancode::from_u8(false, 90)), + Operation::KeyReleased(Scancode::from_u8(false, 90)), + Operation::KeyPressed(Scancode::from_u8(false, 20)), + Operation::KeyReleased(Scancode::from_u8(false, 120)), + Operation::KeyReleased(Scancode::from_u8(false, 90)), + ]; + + let expected_inputs = [ + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 0), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 0), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 0), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 0), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 0), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 20), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 90), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 90), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 90), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 90), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 20), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::empty(), 20), + ]; + + let actual_inputs = db.apply(to_press); + + assert_eq!(actual_inputs.as_slice(), expected_inputs.as_slice()); +} + +#[test] +fn mouse_button_no_duplicate() { + let mut db = Database::default(); + + let to_press = [ + Operation::MouseButtonPressed(MouseButton::Left), + Operation::MouseButtonPressed(MouseButton::Left), + Operation::MouseButtonPressed(MouseButton::Right), + Operation::MouseButtonPressed(MouseButton::Left), + Operation::MouseButtonPressed(MouseButton::Left), + Operation::MouseButtonPressed(MouseButton::Right), + Operation::MouseButtonPressed(MouseButton::Left), + Operation::MouseButtonReleased(MouseButton::Right), + Operation::MouseButtonPressed(MouseButton::Right), + ]; + + let expected_inputs = [ + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::LEFT_BUTTON | PointerFlags::DOWN, + number_of_wheel_rotation_units: 0, + x_position: 0, + y_position: 0, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::RIGHT_BUTTON | PointerFlags::DOWN, + number_of_wheel_rotation_units: 0, + x_position: 0, + y_position: 0, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::RIGHT_BUTTON, + number_of_wheel_rotation_units: 0, + x_position: 0, + y_position: 0, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::RIGHT_BUTTON | PointerFlags::DOWN, + number_of_wheel_rotation_units: 0, + x_position: 0, + y_position: 0, + }), + ]; + + let actual_inputs = db.apply(to_press); + + assert_eq!(actual_inputs.as_slice(), expected_inputs.as_slice()); +} + +#[test] +fn release_all() { + let mut db = Database::default(); + + let ops = [ + Operation::KeyPressed(Scancode::from_u8(false, 0)), + Operation::KeyPressed(Scancode::from_u8(false, 23)), + Operation::KeyPressed(Scancode::from_u8(false, 39)), + Operation::KeyPressed(Scancode::from_u8(true, 19)), + Operation::KeyPressed(Scancode::from_u8(true, 20)), + Operation::KeyPressed(Scancode::from_u8(false, 90)), + Operation::MouseButtonPressed(MouseButton::Left), + Operation::MouseButtonPressed(MouseButton::Left), + Operation::MouseButtonPressed(MouseButton::Right), + Operation::MouseButtonPressed(MouseButton::Left), + Operation::MouseButtonPressed(MouseButton::Middle), + Operation::MouseButtonPressed(MouseButton::Right), + Operation::MouseButtonPressed(MouseButton::Left), + Operation::MouseButtonReleased(MouseButton::Right), + ]; + + let _ = db.apply(ops); + + let expected_inputs = [ + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::LEFT_BUTTON, + number_of_wheel_rotation_units: 0, + x_position: 0, + y_position: 0, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::MIDDLE_BUTTON_OR_WHEEL, + number_of_wheel_rotation_units: 0, + x_position: 0, + y_position: 0, + }), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 0), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 23), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 39), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE, 90), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE | KeyboardFlags::EXTENDED, 19), + FastPathInputEvent::KeyboardEvent(KeyboardFlags::RELEASE | KeyboardFlags::EXTENDED, 20), + ]; + + let actual_inputs = db.release_all(); + + assert_eq!(actual_inputs.as_slice(), expected_inputs.as_slice()); +} + +#[rstest] +#[case(true, false, true, false, SynchronizeFlags::SCROLL_LOCK | SynchronizeFlags::CAPS_LOCK)] +#[case(true, true, true, false, SynchronizeFlags::SCROLL_LOCK | SynchronizeFlags::NUM_LOCK | SynchronizeFlags::CAPS_LOCK)] +#[case(false, false, false, true, SynchronizeFlags::KANA_LOCK)] +fn sync_lock_keys( + #[case] scroll_lock: bool, + #[case] num_lock: bool, + #[case] caps_lock: bool, + #[case] kana_lock: bool, + #[case] expected_flags: SynchronizeFlags, +) { + let event = synchronize_event(scroll_lock, num_lock, caps_lock, kana_lock); + + let FastPathInputEvent::SyncEvent(actual_flags) = event else { + panic!("Unexpected fast path input event"); + }; + + assert_eq!(actual_flags, expected_flags); +} + +#[test] +fn wheel_rotations() { + let mut db = Database::default(); + + let ops = [ + Operation::WheelRotations(WheelRotations { + is_vertical: false, + rotation_units: 2, + }), + Operation::WheelRotations(WheelRotations { + is_vertical: true, + rotation_units: -1, + }), + Operation::WheelRotations(WheelRotations { + is_vertical: false, + rotation_units: -1, + }), + ]; + + let actual_inputs = db.apply(ops); + + let expected_inputs = [ + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::HORIZONTAL_WHEEL, + number_of_wheel_rotation_units: 2, + x_position: 0, + y_position: 0, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::VERTICAL_WHEEL, + number_of_wheel_rotation_units: -1, + x_position: 0, + y_position: 0, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::HORIZONTAL_WHEEL, + number_of_wheel_rotation_units: -1, + x_position: 0, + y_position: 0, + }), + ]; + + assert_eq!(actual_inputs.as_slice(), expected_inputs.as_slice()); +} diff --git a/crates/ironrdp-testsuite-core/tests/input/mod.rs b/crates/ironrdp-testsuite-core/tests/input/mod.rs new file mode 100644 index 00000000..81b00149 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/input/mod.rs @@ -0,0 +1,2 @@ +mod fastpath_packets; +mod smoke; diff --git a/crates/ironrdp-testsuite-core/tests/input/smoke.rs b/crates/ironrdp-testsuite-core/tests/input/smoke.rs new file mode 100644 index 00000000..771ef6bc --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/input/smoke.rs @@ -0,0 +1,161 @@ +use anyhow::{bail, ensure}; +use ironrdp_input::*; +use ironrdp_pdu::input::fast_path::{FastPathInputEvent, KeyboardFlags}; +use proptest::collection::vec; +use proptest::prelude::*; + +fn mouse_button() -> impl Strategy { + prop_oneof![ + Just(MouseButton::Left), + Just(MouseButton::Middle), + Just(MouseButton::Right), + Just(MouseButton::X1), + Just(MouseButton::X2), + ] +} + +fn mouse_button_op() -> impl Strategy { + prop_oneof![ + mouse_button().prop_map(Operation::MouseButtonPressed), + mouse_button().prop_map(Operation::MouseButtonReleased), + ] +} + +fn scancode() -> impl Strategy { + (any::(), any::()).prop_map(Scancode::from) +} + +fn key_op() -> impl Strategy { + prop_oneof![ + scancode().prop_map(Operation::KeyPressed), + scancode().prop_map(Operation::KeyReleased), + ] +} + +fn mouse_position() -> impl Strategy { + (any::(), any::()).prop_map(|(x, y)| MousePosition { x, y }) +} + +#[test] +fn smoke_mouse_buttons() { + let test_impl = |ops: Vec| -> anyhow::Result<()> { + let mut db = Database::default(); + + for op in ops { + db.apply(core::iter::once(op.clone())); + + match op { + Operation::MouseButtonPressed(button) => { + ensure!(db.is_mouse_button_pressed(button)) + } + Operation::MouseButtonReleased(button) => ensure!(!db.is_mouse_button_pressed(button)), + _ => bail!("unexpected case"), + } + } + + Ok(()) + }; + + proptest!(|(ops in vec(mouse_button_op(), 1..5))| { + test_impl(ops).map_err(|e| TestCaseError::fail(format!("{e:#}")))?; + }); +} + +#[test] +fn smoke_mouse_position() { + let test_impl = |ops: Vec| -> anyhow::Result<()> { + let mut db = Database::default(); + + db.apply(ops.iter().copied().map(Operation::MouseMove)); + + let last_position = ops.last().unwrap(); + ensure!(db.mouse_position().eq(last_position)); + + Ok(()) + }; + + proptest!(|(ops in vec(mouse_position(), 1..3))| { + test_impl(ops).map_err(|e| TestCaseError::fail(format!("{e:#}")))?; + }); +} + +#[test] +fn smoke_keyboard() { + let test_impl = |ops: Vec| -> anyhow::Result<()> { + let mut db = Database::default(); + + for op in ops { + let packets = db.apply(core::iter::once(op.clone())); + + match op { + Operation::KeyPressed(scancode) => { + ensure!(packets.len() <= 2); + ensure!(db.is_key_pressed(scancode)); + + let mut packets = packets.into_iter(); + + match (packets.next(), packets.next()) { + (None, None) => {} + (None, Some(_)) => unreachable!(), + (Some(pressed_packet), None) => { + if let FastPathInputEvent::KeyboardEvent(flags, packet_scancode) = pressed_packet { + ensure!(!flags.contains(KeyboardFlags::RELEASE)); + ensure!( + scancode + == Scancode::from_u8(flags.contains(KeyboardFlags::EXTENDED), packet_scancode) + ) + } else { + bail!("unexpected packet emitted"); + } + } + (Some(released_packet), Some(pressed_packet)) => { + if let FastPathInputEvent::KeyboardEvent(flags, packet_scancode) = released_packet { + ensure!(flags.contains(KeyboardFlags::RELEASE)); + ensure!( + scancode + == Scancode::from_u8(flags.contains(KeyboardFlags::EXTENDED), packet_scancode) + ) + } else { + bail!("unexpected packet emitted"); + } + + if let FastPathInputEvent::KeyboardEvent(flags, packet_scancode) = pressed_packet { + ensure!(!flags.contains(KeyboardFlags::RELEASE)); + ensure!( + scancode + == Scancode::from_u8(flags.contains(KeyboardFlags::EXTENDED), packet_scancode) + ) + } else { + bail!("unexpected packet emitted"); + } + } + } + } + Operation::KeyReleased(scancode) => { + ensure!(packets.len() <= 1); + ensure!(!db.is_key_pressed(scancode)); + + let packet = packets.into_iter().next(); + + if let Some(packet) = packet { + if let FastPathInputEvent::KeyboardEvent(flags, packet_scancode) = packet { + ensure!(flags.contains(KeyboardFlags::RELEASE)); + ensure!( + scancode == Scancode::from_u8(flags.contains(KeyboardFlags::EXTENDED), packet_scancode) + ) + } else { + bail!("unexpected packet emitted"); + } + } + } + _ => bail!("unexpected case"), + } + } + + Ok(()) + }; + + proptest!(|(ops in vec(key_op(), 1..5))| { + test_impl(ops).map_err(|e| TestCaseError::fail(format!("{e:#}")))?; + }); +} diff --git a/crates/ironrdp-testsuite-core/tests/main.rs b/crates/ironrdp-testsuite-core/tests/main.rs new file mode 100644 index 00000000..ba011f85 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/main.rs @@ -0,0 +1,28 @@ +#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary +#![allow(clippy::panic, reason = "panic is acceptable in tests")] +#![allow(clippy::unwrap_used, reason = "unwrap is fine in tests")] +//! Integration Tests (IT) +//! +//! Integration tests are all contained in this single crate, and organized in modules. +//! This is to prevent `rustc` to re-link the library crates with each of the integration +//! tests (one for each *.rs file / test crate under the `tests/` folder). +//! Performance implication: https://github.com/rust-lang/cargo/pull/5022#issuecomment-364691154 +//! +//! This is also good for execution performance. +//! Cargo will run all tests from a single binary in parallel, but +//! binaries themselves are run sequentially. + +mod clipboard; +mod displaycontrol; +mod dvc; +mod fuzz_regression; +mod graphics; +mod input; +mod pcb; +mod pdu; +mod propertyset; +mod rdcleanpath; +mod rdpsnd; +mod server; +mod server_name; +mod session; diff --git a/crates/ironrdp-testsuite-core/tests/pcb.rs b/crates/ironrdp-testsuite-core/tests/pcb.rs new file mode 100644 index 00000000..c31dd812 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pcb.rs @@ -0,0 +1,168 @@ +use expect_test::expect; +use ironrdp_pdu::pcb::*; +use ironrdp_testsuite_core::encode_decode_test; + +encode_decode_test! { + v1: + PreconnectionBlob { + version: PcbVersion::V1, + id: 4_005_992_939, + v2_payload: None, + }, + [ + 0x10, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::cbSize = 0x10 = 16 bytes + 0x00, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Flags = 0 + 0x01, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Version = 1 + 0xeb, 0x99, 0xc6, 0xee, // -> RDP_PRECONNECTION_PDU_V1::Id = 0xEEC699EB = 4005992939 + ]; + + v2: + PreconnectionBlob { + version: PcbVersion::V2, + id: 0, + v2_payload: Some(String::from("TestVM")), + }, + [ + 0x20, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::cbSize = 0x20 = 32 bytes + 0x00, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Flags = 0 + 0x02, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Version = 2 + 0x00, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Id = 0 + 0x07, 0x00, // -> RDP_PRECONNECTION_PDU_V2::cchPCB = 0x7 = 7 characters + 0x54, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x56, 0x00, 0x4d, 0x00, 0x00, + 0x00, // -> RDP_PRECONNECTION_PDU_V2::wszPCB -> "TestVM\0" + ]; + + v2_jwt: + PreconnectionBlob { + version: PcbVersion::V2, + id: 0, + v2_payload: Some(concat!( + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkFTU09DSUFUSU9OIn0.eyJkc3RfaHN0IjoidG", + "NwOi8vMTAuMTAuMC4yOjIyIiwiZXhwIjoxNjg3ODE1OTIyLCJpYXQiOjE2ODc4MTU2MjIsImpldF9haWQi", + "OiI4ZDA2NjBhNy0xNDlkLTRkMDctOTUwNC0zNmM5NDhiMjQxMzYiLCJqZXRfYXAiOiJzc2giLCJqZXRfY2", + "0iOiJmd2QiLCJqZXRfZ3dfaWQiOiJiOGExYTA2NC0xOTg1LTRkNzktYjBjNC05YWMxMWIzZTg0ZTEiLCJq", + "dGkiOiJmNmMyYWE4OC00MzZjLTRkN2UtODc3ZC02OGM3Y2NhNmEwZDYiLCJuYmYiOjE2ODc4MTU2MjJ9.D", + "P6pKkMpAVpOIxrywOkbzTKuI0zdxKN-d4XguRI2Pb48BubTQ7EY1xNp8H_kHxaQ6Fr46fBA5fmmjGtnW_2", + "exMOu8ZTqduSPXrUVOogE4mBZMAYjVIMnBhsLOfx30AZS8YKCsL8FNnNJZBnFpgaJe8Mz9eCrhPWiDYD10", + "p2dMBcElUOWU9Gh0YWlbIcRS6zkp6vAARdWGn0L98HgyxOFqnihsdmAESsm9ma7EVeTLoXJMYVCUwPj6tW", + "QOs9SGNnNoGShJLQbPeHUB6lGJs_g1V7ojSK-0K0rVtBRI6iG0a8Q6sMLomRoM7IKwVMHCfYte6I3fLaY1", + "_b3SrwXIWjn5A" + ).to_owned()), + }, + hex::decode(concat!( + "f2050000000000000200000000000000f002650079004a00680062004700630069004f0069004a00530055007a0049", + "0031004e0069004900730049006e0052003500630043004900360049006b0070005800560043004900730049006d00", + "4e003000650053004900360049006b0046005400550030003900440053005500460055005300550039004f0049006e", + "0030002e00650079004a006b006300330052006600610048004e00300049006a006f006900640047004e0077004f00", + "6900380076004d005400410075004d005400410075004d004300340079004f006a004900790049006900770069005a", + "0058006800770049006a006f0078004e006a00670033004f004400450031004f005400490079004c0043004a007000", + "59005800510069004f006a00450032004f004400630034004d005400550032004d006a004900730049006d0070006c", + "00640046003900680061005700510069004f006900490034005a004400410032004e006a00420068004e0079003000", + "78004e0044006c006b004c00540052006b004d004400630074004f005400550077004e00430030007a004e006d004d", + "0035004e004400680069004d006a00510078004d007a00590069004c0043004a0071005a0058005200660059005800", + "410069004f0069004a007a0063003200670069004c0043004a0071005a0058005200660059003200300069004f0069", + "004a006d0064003200510069004c0043004a0071005a005800520066005a0033006400660061005700510069004f00", + "69004a0069004f0047004500780059005400410032004e004300300078004f005400670031004c00540052006b004e", + "007a006b00740059006a0042006a004e00430030003500590057004d0078004d00570049007a005a00540067003000", + "5a005400450069004c0043004a007100640047006b0069004f0069004a006d004e006d004d00790059005700450034", + "004f004300300030004d007a005a006a004c00540052006b004e003200550074004f004400630033005a0043003000", + "32004f0047004d003300590032004e0068004e006d00450077005a004400590069004c0043004a00750059006d0059", + "0069004f006a00450032004f004400630034004d005400550032004d006a004a0039002e0044005000360070004b00", + "6b004d0070004100560070004f00490078007200790077004f006b0062007a0054004b007500490030007a00640078", + "004b004e002d0064003400580067007500520049003200500062003400380042007500620054005100370045005900", + "310078004e007000380048005f006b00480078006100510036004600720034003600660042004100350066006d006d", + "006a00470074006e0057005f003200650078004d004f00750038005a00540071006400750053005000580072005500", + "56004f006f006700450034006d0042005a004d00410059006a00560049004d006e004200680073004c004f00660078", + "003300300041005a005300380059004b00430073004c00380046004e006e004e004a005a0042006e00460070006700", + "61004a00650038004d007a0039006500430072006800500057006900440059004400310030007000320064004d0042", + "00630045006c0055004f00570055003900470068003000590057006c006200490063005200530036007a006b007000", + "360076004100410052006400570047006e0030004c003900380048006700790078004f00460071006e006900680073", + "0064006d0041004500530073006d0039006d006100370045005600650054004c006f0058004a004d00590056004300", + "5500770050006a0036007400570051004f0073003900530047004e006e004e006f004700530068004a004c00510062", + "005000650048005500420036006c0047004a0073005f0067003100560037006f006a0053004b002d0030004b003000", + "7200560074004200520049003600690047003000610038005100360073004d004c006f006d0052006f004d00370049", + "004b00770056004d0048004300660059007400650036004900330066004c006100590031005f006200330053007200", + "77005800490057006a006e00350041000000" + )).expect("pcb_v2_with_jwt payload"); +} + +const PRECONNECTION_PDU_V1_NULL_SIZE_BUF: [u8; 16] = [ + 0x00, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::cbSize = 0x00 = 0 bytes + 0x00, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Flags = 0 + 0x01, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Version = 1 + 0xeb, 0x99, 0xc6, 0xee, // -> RDP_PRECONNECTION_PDU_V1::Id = 0xEEC699EB = 4005992939 +]; + +#[test] +fn null_size() { + let e = ironrdp_core::decode::(&PRECONNECTION_PDU_V1_NULL_SIZE_BUF) + .err() + .unwrap(); + + expect![[r#" + Error { + context: "PreconnectionBlob", + kind: InvalidField { + field: "cbSize", + reason: "advertised size too small for Preconnection PDU V1", + }, + source: None, + } + "#]] + .assert_debug_eq(&e); +} + +const PRECONNECTION_PDU_V1_LARGE_SIZE_BUF: [u8; 16] = [ + 0xff, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::cbSize = 0xff + 0x00, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Flags = 0 + 0x01, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Version = 1 + 0xeb, 0x99, 0xc6, 0xee, // -> RDP_PRECONNECTION_PDU_V1::Id = 0xEEC699EB = 4005992939 +]; + +#[test] +fn truncated() { + let e = ironrdp_core::decode::(&PRECONNECTION_PDU_V1_LARGE_SIZE_BUF) + .err() + .unwrap(); + + expect![[r#" + Error { + context: "::decode", + kind: NotEnoughBytes { + received: 0, + expected: 239, + }, + source: None, + } + "#]] + .assert_debug_eq(&e); +} + +const PRECONNECTION_PDU_V2_LARGE_PAYLOAD_SIZE_BUF: [u8; 32] = [ + 0x20, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::cbSize = 0x20 = 32 bytes + 0x00, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Flags = 0 + 0x02, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Version = 2 + 0x00, 0x00, 0x00, 0x00, // -> RDP_PRECONNECTION_PDU_V1::Id = 0 + 0xff, 0x00, // -> RDP_PRECONNECTION_PDU_V2::cchPCB = 0xff + 0x54, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x56, 0x00, 0x4d, 0x00, 0x00, + 0x00, // -> RDP_PRECONNECTION_PDU_V2::wszPCB -> "TestVM\0" +]; + +#[test] +fn pcb_v2_string_too_big() { + let e = ironrdp_core::decode::(&PRECONNECTION_PDU_V2_LARGE_PAYLOAD_SIZE_BUF) + .err() + .unwrap(); + + expect![[r#" + Error { + context: "PreconnectionBlob", + kind: InvalidField { + field: "cchPCB", + reason: "PCB string bigger than advertised size", + }, + source: None, + } + "#]] + .assert_debug_eq(&e); +} diff --git a/crates/ironrdp-testsuite-core/tests/pdu/gcc.rs b/crates/ironrdp-testsuite-core/tests/pdu/gcc.rs new file mode 100644 index 00000000..718ccca9 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/gcc.rs @@ -0,0 +1,869 @@ +use ironrdp_core::{decode, encode_vec, DecodeErrorKind, Encode as _, EncodeErrorKind, ReadCursor}; +use ironrdp_pdu::gcc::*; +use ironrdp_testsuite_core::cluster_data::*; +use ironrdp_testsuite_core::conference_create::*; +use ironrdp_testsuite_core::core_data::*; +use ironrdp_testsuite_core::gcc::*; +use ironrdp_testsuite_core::network_data::*; +use ironrdp_testsuite_core::security_data::*; + +#[test] +fn from_buffer_correctly_parses_client_gcc_blocks_without_optional_data_blocks() { + let buffer = CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER; + + assert_eq!(*CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS, decode(buffer.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_client_gcc_blocks_with_one_optional_data_blocks() { + let buffer = CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD_BUFFER; + + assert_eq!( + *CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD, + decode(buffer.as_slice()).unwrap() + ); +} + +#[test] +fn from_buffer_correctly_parses_client_gcc_blocks_with_all_optional_data_blocks() { + let buffer = CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS_BUFFER; + + assert_eq!(*CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS, decode(buffer.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_client_gcc_blocks_with_optional_data_blocks_in_different_order() { + let buffer = CLIENT_GCC_WITH_OPTIONAL_FIELDS_IN_DIFFERENT_ORDER_BUFFER; + + assert_eq!(*CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS, decode(buffer.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_fails_on_invalid_gcc_type_for_client_gcc_blocks() { + let mut buffer = CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS_BUFFER; + buffer[CLIENT_GCC_CORE_BLOCK_BUFFER.len()] = 0x00; + + assert!(decode::(buffer.as_slice()).is_err()); +} + +#[test] +fn to_buffer_correctly_serializes_client_gcc_blocks_without_optional_data_blocks() { + let data = CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer = CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_client_gcc_blocks_with_one_optional_data_blocks() { + let data = CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD.clone(); + let expected_buffer = CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_client_gcc_blocks_with_all_optional_data_blocks() { + let data = CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS.clone(); + let expected_buffer = CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_client_gcc_blocks_without_optional_data_blocks() { + let data = CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_client_gcc_blocks_with_one_optional_data_blocks() { + let data = CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD.clone(); + let expected_buffer_len = CLIENT_GCC_WITH_CLUSTER_OPTIONAL_FIELD_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_client_gcc_blocks_with_all_optional_data_blocks() { + let data = CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = CLIENT_GCC_WITH_ALL_OPTIONAL_FIELDS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_server_gcc_blocks_without_optional_data_blocks() { + let buffer = SERVER_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER; + + assert_eq!(*SERVER_GCC_WITHOUT_OPTIONAL_FIELDS, decode(buffer.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_server_gcc_blocks_with_optional_data_blocks() { + let buffer = SERVER_GCC_WITH_OPTIONAL_FIELDS_BUFFER; + + assert_eq!(*SERVER_GCC_WITH_OPTIONAL_FIELDS, decode(buffer.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_server_gcc_blocks_with_optional_data_blocks_in_different_order() { + let buffer = SERVER_GCC_WITH_OPTIONAL_FIELDS_IN_DIFFERENT_ORDER_BUFFER; + + assert_eq!(*SERVER_GCC_WITH_OPTIONAL_FIELDS, decode(buffer.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_fails_on_invalid_gcc_type_for_server_gcc_blocks() { + let mut buffer = SERVER_GCC_WITH_OPTIONAL_FIELDS_BUFFER; + buffer[SERVER_GCC_CORE_BLOCK_BUFFER.len()] = 0x00; + + assert!(decode::(buffer.as_slice()).is_err()); +} + +#[test] +fn to_buffer_correctly_serializes_server_gcc_blocks_without_optional_data_blocks() { + let data = SERVER_GCC_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer = SERVER_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_server_gcc_blocks_with_optional_data_blocks() { + let data = SERVER_GCC_WITH_OPTIONAL_FIELDS.clone(); + let expected_buffer = SERVER_GCC_WITH_OPTIONAL_FIELDS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_server_gcc_blocks_without_optional_data_blocks() { + let data = SERVER_GCC_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = SERVER_GCC_WITHOUT_OPTIONAL_FIELDS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_server_gcc_blocks_with_optional_data_blocks() { + let data = SERVER_GCC_WITH_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = SERVER_GCC_WITH_OPTIONAL_FIELDS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_handles_invalid_lengths_in_user_data_header() { + let buffer: [u8; 4] = [0x01, 0xc0, 0x00, 0x00]; + let mut cur = ReadCursor::new(&buffer); + + assert!(UserDataHeader::decode::(&mut cur).is_err()); +} + +#[test] +fn from_buffer_correctly_parses_client_cluster_data() { + let buffer = CLUSTER_DATA_BUFFER.as_ref(); + + assert_eq!(*CLUSTER_DATA, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_client_cluster_data() { + let data = CLUSTER_DATA.clone(); + let expected_buffer = CLUSTER_DATA_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_client_cluster_data() { + let data = CLUSTER_DATA.clone(); + let expected_buffer_len = CLUSTER_DATA_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_client_core_data_without_optional_fields() { + let buffer = CLIENT_CORE_DATA_BUFFER.as_ref(); + + assert_eq!(*CLIENT_CORE_DATA_WITHOUT_OPTIONAL_FIELDS, decode(buffer).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_client_core_data_without_few_optional_fields() { + let buffer = CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL_BUFFER; + + assert_eq!( + *CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL, + decode(buffer.as_slice()).unwrap() + ); +} + +#[test] +fn from_buffer_correctly_parses_client_core_data_with_all_optional_fields() { + let buffer = CLIENT_OPTIONAL_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_BUFFER; + + assert_eq!( + *CLIENT_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS, + decode(buffer.as_slice()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_client_core_data_without_optional_fields() { + let core_data = CLIENT_CORE_DATA_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer = CLIENT_CORE_DATA_BUFFER.as_ref(); + + let buf = encode_vec(&core_data).unwrap(); + + assert_eq!(expected_buffer, buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_client_core_data_without_few_optional_fields() { + let core_data = CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL.clone(); + let expected_buffer = CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL_BUFFER; + + let buf = encode_vec(&core_data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_client_core_data_with_all_optional_fields() { + let core_data = CLIENT_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS.clone(); + let expected_buffer = CLIENT_OPTIONAL_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_BUFFER; + + let buf = encode_vec(&core_data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_client_core_data_without_optional_fields() { + let data = CLIENT_CORE_DATA_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = CLIENT_CORE_DATA_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_client_core_data_without_few_optional_fields() { + let data = CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL.clone(); + let expected_buffer_len = CLIENT_OPTIONAL_CORE_DATA_TO_SERVER_SELECTED_PROTOCOL_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_client_core_data_with_all_optional_fields() { + let data = CLIENT_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = CLIENT_OPTIONAL_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn client_color_depth_is_color_depth_if_post_beta_color_depth_is_absent() { + let buffer = CLIENT_CORE_DATA_BUFFER.as_ref(); + + let core_data: ClientCoreData = decode(buffer).unwrap(); + let expected_client_color_depth: ClientColorDepth = From::from(core_data.color_depth); + + assert_eq!(expected_client_color_depth, core_data.client_color_depth()); +} + +#[test] +fn client_color_depth_is_post_beta_color_depth_if_high_color_depth_is_absent() { + let buffer = CLIENT_OPTIONAL_CORE_DATA_TO_HIGH_COLOR_DEPTH_BUFFER_BUFFER; + + let core_data: ClientCoreData = decode(buffer.as_slice()).unwrap(); + let expected_client_color_depth: ClientColorDepth = + From::from(core_data.optional_data.post_beta2_color_depth.unwrap()); + + assert_eq!(expected_client_color_depth, core_data.client_color_depth()); +} + +#[test] +fn client_color_depth_is_high_color_depth_if_want_32_bpp_flag_is_absent() { + let buffer = CLIENT_OPTIONAL_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_BUFFER; + + let core_data: ClientCoreData = decode(buffer.as_slice()).unwrap(); + let expected_client_color_depth: ClientColorDepth = From::from(core_data.optional_data.high_color_depth.unwrap()); + + assert_eq!(expected_client_color_depth, core_data.client_color_depth()); +} + +#[test] +fn client_color_depth_is_32_bpp_if_want_32_bpp_flag_is_set() { + let buffer = CLIENT_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_WITH_WANT_32_BPP_EARLY_FLAG_BUFFER.clone(); + let expected_core_data = CLIENT_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_WITH_WANT_32_BPP_EARLY_FLAG.clone(); + let expected_client_color_depth = ClientColorDepth::Bpp32; + + let core_data: ClientCoreData = decode(buffer.as_slice()).unwrap(); + + assert_eq!(expected_core_data, core_data); + assert_eq!(expected_client_color_depth, core_data.client_color_depth()); +} + +#[test] +fn from_buffer_correctly_parses_server_core_data_without_optional_fields() { + let buffer = SERVER_CORE_DATA_BUFFER.as_ref(); + + assert_eq!(*SERVER_CORE_DATA, decode(buffer).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_server_core_data_without_few_optional_fields() { + let buffer = SERVER_CORE_DATA_TO_REQUESTED_PROTOCOL_BUFFER; + + assert_eq!(*SERVER_CORE_DATA_TO_FLAGS, decode(buffer.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_server_core_data_with_all_optional_fields() { + let buffer = SERVER_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_BUFFER; + + assert_eq!( + *SERVER_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS, + decode(buffer.as_slice()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_server_core_data_without_optional_fields() { + let data = SERVER_CORE_DATA.clone(); + let expected_buffer = SERVER_CORE_DATA_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_server_core_data_without_few_optional_fields() { + let data = SERVER_CORE_DATA_TO_FLAGS.clone(); + let expected_buffer = SERVER_CORE_DATA_TO_REQUESTED_PROTOCOL_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_server_core_data_with_all_optional_fields() { + let core_data = SERVER_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS.clone(); + let expected_buffer = SERVER_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_BUFFER; + + let buf = encode_vec(&core_data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_server_core_data_without_optional_fields() { + let data = SERVER_CORE_DATA.clone(); + let expected_buffer_len = SERVER_CORE_DATA_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_server_core_data_without_few_optional_fields() { + let data = SERVER_CORE_DATA_TO_FLAGS.clone(); + let expected_buffer_len = SERVER_CORE_DATA_TO_REQUESTED_PROTOCOL_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_server_core_data_with_all_optional_fields() { + let data = SERVER_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = SERVER_CORE_DATA_WITH_ALL_OPTIONAL_FIELDS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_server_message_channel_data() { + let buffer = ironrdp_testsuite_core::message_channel_data::SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER.as_ref(); + + assert_eq!( + ironrdp_testsuite_core::message_channel_data::SERVER_GCC_MESSAGE_CHANNEL_BLOCK, + decode(buffer).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_server_message_channel_data() { + let data = ironrdp_testsuite_core::message_channel_data::SERVER_GCC_MESSAGE_CHANNEL_BLOCK.clone(); + let expected_buffer = ironrdp_testsuite_core::message_channel_data::SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_server_message_channel_data() { + let data = ironrdp_testsuite_core::message_channel_data::SERVER_GCC_MESSAGE_CHANNEL_BLOCK.clone(); + let expected_buffer_len = + ironrdp_testsuite_core::message_channel_data::SERVER_GCC_MESSAGE_CHANNEL_BLOCK_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_client_monitor_data_without_monitors() { + let buffer = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITHOUT_MONITORS_BUFFER.as_ref(); + + assert_eq!( + *ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITHOUT_MONITORS, + decode(buffer).unwrap() + ); +} + +#[test] +fn from_buffer_correctly_parses_client_monitor_data_with_monitors() { + let buffer = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITH_MONITORS_BUFFER.as_ref(); + + assert_eq!( + *ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITH_MONITORS, + decode(buffer).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_client_monitor_data_without_monitors() { + let data = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITHOUT_MONITORS.clone(); + let expected_buffer = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITHOUT_MONITORS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_client_monitor_data_with_monitors() { + let data = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITH_MONITORS.clone(); + let expected_buffer = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITH_MONITORS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_client_monitor_data_without_monitors() { + let data = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITHOUT_MONITORS.clone(); + let expected_buffer_len = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITHOUT_MONITORS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_client_monitor_data_with_monitors() { + let data = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITH_MONITORS.clone(); + let expected_buffer_len = ironrdp_testsuite_core::monitor_data::MONITOR_DATA_WITH_MONITORS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_client_monitor_extended_data_without_monitors() { + let buffer = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITHOUT_MONITORS_BUFFER.as_ref(); + + assert_eq!( + *ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITHOUT_MONITORS, + decode(buffer).unwrap() + ); +} + +#[test] +fn from_buffer_correctly_parses_client_monitor_extended_data_with_monitors() { + let buffer = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITH_MONITORS_BUFFER.as_ref(); + + assert_eq!( + *ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITH_MONITORS, + decode(buffer).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_client_monitor_extended_data_without_monitors() { + let data = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITHOUT_MONITORS.clone(); + let expected_buffer = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITHOUT_MONITORS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_client_monitor_extended_data_with_monitors() { + let data = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITH_MONITORS.clone(); + let expected_buffer = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITH_MONITORS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_client_monitor_extended_data_without_monitors() { + let data = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITHOUT_MONITORS.clone(); + let expected_buffer_len = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITHOUT_MONITORS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_client_monitor_extended_data_with_monitors() { + let data = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITH_MONITORS.clone(); + let expected_buffer_len = ironrdp_testsuite_core::monitor_extended_data::MONITOR_DATA_WITH_MONITORS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_server_multi_transport_channel_data() { + let buffer = + ironrdp_testsuite_core::multi_transport_channel_data::SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER.as_ref(); + + assert_eq!( + *ironrdp_testsuite_core::multi_transport_channel_data::SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK, + decode(buffer).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_server_multi_transport_channel_data() { + let data = ironrdp_testsuite_core::multi_transport_channel_data::SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK.clone(); + let expected_buffer = + ironrdp_testsuite_core::multi_transport_channel_data::SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_server_multi_transport_channel_data() { + let data = ironrdp_testsuite_core::multi_transport_channel_data::SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK.clone(); + let expected_buffer_len = + ironrdp_testsuite_core::multi_transport_channel_data::SERVER_GCC_MULTI_TRANSPORT_CHANNEL_BLOCK_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_client_network_data_without_channels() { + let buffer = CLIENT_NETWORK_DATA_WITHOUT_CHANNELS_BUFFER.as_ref(); + + assert_eq!(*CLIENT_NETWORK_DATA_WITHOUT_CHANNELS, decode(buffer).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_client_network_data_with_channels() { + let buffer = CLIENT_NETWORK_DATA_WITH_CHANNELS_BUFFER.as_ref(); + + assert_eq!(*CLIENT_NETWORK_DATA_WITH_CHANNELS, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_client_network_data_without_channels() { + let data = CLIENT_NETWORK_DATA_WITHOUT_CHANNELS.clone(); + let expected_buffer = CLIENT_NETWORK_DATA_WITHOUT_CHANNELS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_client_network_data_with_channels() { + let data = CLIENT_NETWORK_DATA_WITH_CHANNELS.clone(); + let expected_buffer = CLIENT_NETWORK_DATA_WITH_CHANNELS_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_client_network_data_without_channels() { + let data = CLIENT_NETWORK_DATA_WITHOUT_CHANNELS.clone(); + let expected_buffer_len = CLIENT_NETWORK_DATA_WITHOUT_CHANNELS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_client_network_data_with_channels() { + let data = CLIENT_NETWORK_DATA_WITH_CHANNELS.clone(); + let expected_buffer_len = CLIENT_NETWORK_DATA_WITH_CHANNELS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_server_network_data_without_channels_id() { + let buffer = SERVER_NETWORK_DATA_WITHOUT_CHANNELS_ID_BUFFER.as_ref(); + + assert_eq!(*SERVER_NETWORK_DATA_WITHOUT_CHANNELS_ID, decode(buffer).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_server_network_data_with_channels_id() { + let buffer = SERVER_NETWORK_DATA_WITH_CHANNELS_ID_BUFFER.as_ref(); + + assert_eq!(*SERVER_NETWORK_DATA_WITH_CHANNELS_ID, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_server_network_data_without_channels_id() { + let data = SERVER_NETWORK_DATA_WITHOUT_CHANNELS_ID.clone(); + let expected_buffer = SERVER_NETWORK_DATA_WITHOUT_CHANNELS_ID_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_server_network_data_with_channels_id() { + let data = SERVER_NETWORK_DATA_WITH_CHANNELS_ID.clone(); + let expected_buffer = SERVER_NETWORK_DATA_WITH_CHANNELS_ID_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_server_network_data_without_channels_id() { + let data = SERVER_NETWORK_DATA_WITHOUT_CHANNELS_ID.clone(); + let expected_buffer_len = SERVER_NETWORK_DATA_WITHOUT_CHANNELS_ID_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_server_network_data_with_channels_id() { + let data = SERVER_NETWORK_DATA_WITH_CHANNELS_ID.clone(); + let expected_buffer_len = SERVER_NETWORK_DATA_WITH_CHANNELS_ID_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_client_security_data() { + let buffer = CLIENT_SECURITY_DATA_BUFFER.as_ref(); + + assert_eq!(*CLIENT_SECURITY_DATA, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_client_security_data() { + let security_data = CLIENT_SECURITY_DATA.clone(); + let expected_buffer = CLIENT_SECURITY_DATA_BUFFER; + + let buf = encode_vec(&security_data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_client_security_data() { + let data = CLIENT_SECURITY_DATA.clone(); + let expected_buffer_len = CLIENT_SECURITY_DATA_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_server_security_data_without_optional_fields() { + let buffer = SERVER_SECURITY_DATA_WITHOUT_OPTIONAL_FIELDS_BUFFER.as_ref(); + + assert_eq!(*SERVER_SECURITY_DATA_WITHOUT_OPTIONAL_FIELDS, decode(buffer).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_server_security_data_with_all_fields() { + let buffer = SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_BUFFER; + + assert_eq!( + *SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS, + decode(buffer.as_slice()).unwrap() + ); +} + +#[test] +fn from_buffer_server_security_data_fails_with_invalid_server_random_length() { + let buffer = SERVER_SECURITY_DATA_WITH_INVALID_SERVER_RANDOM_BUFFER; + + match decode::(buffer.as_slice()) { + Err(e) if matches!(e.kind(), DecodeErrorKind::InvalidField { .. }) => (), + res => panic!("Expected the invalid server random length error, got: {res:?}"), + }; +} + +#[test] +fn to_buffer_correctly_serializes_server_security_data_without_optional_fields() { + let security_data = SERVER_SECURITY_DATA_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer = SERVER_SECURITY_DATA_WITHOUT_OPTIONAL_FIELDS_BUFFER; + + let buf = encode_vec(&security_data).unwrap(); + + assert_eq!(expected_buffer.as_ref(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_server_security_data_with_optional_fields() { + let security_data = SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS.clone(); + + let buf = encode_vec(&security_data).unwrap(); + assert_eq!(buf, SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_BUFFER.as_slice()); +} + +#[test] +fn to_buffer_server_security_data_fails_on_mismatch_of_required_and_optional_fields() { + let security_data = SERVER_SECURITY_DATA_WITH_MISMATCH_OF_REQUIRED_AND_OPTIONAL_FIELDS.clone(); + + match encode_vec(&security_data) { + Err(e) if matches!(e.kind(), EncodeErrorKind::InvalidField { .. }) => (), + res => panic!("Expected the invalid input error, got: {res:?}"), + }; +} + +#[test] +fn buffer_length_is_correct_for_server_security_data_without_optional_fields() { + let data = SERVER_SECURITY_DATA_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = SERVER_SECURITY_DATA_WITHOUT_OPTIONAL_FIELDS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_server_security_data_with_optional_fields() { + let data = SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = SERVER_SECURITY_DATA_WITH_OPTIONAL_FIELDS_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_conference_create_request() { + assert_eq!( + *CONFERENCE_CREATE_REQUEST, + decode(CONFERENCE_CREATE_REQUEST_BUFFER.as_slice()).unwrap() + ); +} + +#[test] +fn to_buffer_correctly_serializes_conference_create_request() { + let data = CONFERENCE_CREATE_REQUEST.clone(); + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(buf.as_slice(), CONFERENCE_CREATE_REQUEST_BUFFER.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_conference_create_request() { + let len = CONFERENCE_CREATE_REQUEST.size(); + assert_eq!(len, CONFERENCE_CREATE_REQUEST_BUFFER.len()); +} + +#[test] +fn from_buffer_correctly_parses_conference_create_response() { + let buffer = CONFERENCE_CREATE_RESPONSE_BUFFER; + + assert_eq!(*CONFERENCE_CREATE_RESPONSE, decode(buffer.as_slice()).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_conference_create_response() { + let data = CONFERENCE_CREATE_RESPONSE.clone(); + let expected_buffer = CONFERENCE_CREATE_RESPONSE_BUFFER; + + let buf = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer.as_slice(), buf.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_conference_create_response() { + let data = CONFERENCE_CREATE_RESPONSE.clone(); + let expected_buffer_len = CONFERENCE_CREATE_RESPONSE_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} diff --git a/crates/ironrdp-testsuite-core/tests/pdu/gfx.rs b/crates/ironrdp-testsuite-core/tests/pdu/gfx.rs new file mode 100644 index 00000000..098d3c1d --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/gfx.rs @@ -0,0 +1,446 @@ +use ironrdp_core::{decode, decode_cursor, encode_vec, Encode as _, ReadCursor}; +use ironrdp_testsuite_core::gfx::*; +use ironrdp_testsuite_core::graphics_messages::*; + +#[test] +fn from_buffer_correctly_parses_server_pdu() { + let buffer = HEADER_WITH_WIRE_TO_SURFACE_1_BUFFER.as_ref(); + + assert_eq!(*HEADER_WITH_WIRE_TO_SURFACE_1, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_server_pdu() { + let buffer = encode_vec(&*HEADER_WITH_WIRE_TO_SURFACE_1).unwrap(); + + assert_eq!(buffer, HEADER_WITH_WIRE_TO_SURFACE_1_BUFFER.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_server_pdu() { + assert_eq!( + HEADER_WITH_WIRE_TO_SURFACE_1_BUFFER.len(), + HEADER_WITH_WIRE_TO_SURFACE_1.size() + ); +} + +#[test] +fn from_buffer_correctly_parses_client_pdu() { + let buffer = HEADER_WITH_FRAME_ACKNOWLEDGE_BUFFER.as_ref(); + + assert_eq!(*HEADER_WITH_FRAME_ACKNOWLEDGE, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_client_pdu() { + let buffer = encode_vec(&*HEADER_WITH_FRAME_ACKNOWLEDGE).unwrap(); + + assert_eq!(buffer, HEADER_WITH_FRAME_ACKNOWLEDGE_BUFFER.as_slice()); +} + +#[test] +fn buffer_length_is_correct_for_client_pdu() { + assert_eq!( + HEADER_WITH_FRAME_ACKNOWLEDGE_BUFFER.len(), + HEADER_WITH_FRAME_ACKNOWLEDGE.size() + ); +} + +#[test] +fn from_buffer_correctly_parses_wire_to_surface_1_pdu() { + let buffer = WIRE_TO_SURFACE_1_BUFFER.as_ref(); + + assert_eq!(*WIRE_TO_SURFACE_1, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_wire_to_surface_1_pdu() { + let buffer = encode_vec(&*WIRE_TO_SURFACE_1).unwrap(); + + assert_eq!(buffer, WIRE_TO_SURFACE_1_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_wire_to_surface_1_pdu() { + assert_eq!(WIRE_TO_SURFACE_1_BUFFER.len(), WIRE_TO_SURFACE_1.size()); +} + +#[test] +fn from_buffer_correctly_parses_wire_to_surface_2_pdu() { + let buffer = WIRE_TO_SURFACE_2_BUFFER.as_ref(); + + assert_eq!(*WIRE_TO_SURFACE_2, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_wire_to_surface_2_pdu() { + let buffer = encode_vec(&*WIRE_TO_SURFACE_2).unwrap(); + + assert_eq!(buffer, WIRE_TO_SURFACE_2_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_wire_to_surface_2_pdu() { + assert_eq!(WIRE_TO_SURFACE_2_BUFFER.len(), WIRE_TO_SURFACE_2.size()); +} + +#[test] +fn from_buffer_correctly_parses_delete_encoding_context_pdu() { + let buffer = DELETE_ENCODING_CONTEXT_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*DELETE_ENCODING_CONTEXT, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_delete_encoding_context_pdu() { + let buffer = encode_vec(&*DELETE_ENCODING_CONTEXT).unwrap(); + + assert_eq!(buffer, DELETE_ENCODING_CONTEXT_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_delete_encoding_context_pdu() { + assert_eq!(DELETE_ENCODING_CONTEXT_BUFFER.len(), DELETE_ENCODING_CONTEXT.size()); +} + +#[test] +fn from_buffer_correctly_parses_solid_fill_pdu() { + let buffer = SOLID_FILL_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*SOLID_FILL, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_solid_fill_pdu() { + let buffer = encode_vec(&*SOLID_FILL).unwrap(); + assert_eq!(buffer, SOLID_FILL_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_solid_fill_pdu() { + assert_eq!(SOLID_FILL_BUFFER.len(), SOLID_FILL.size()); +} + +#[test] +fn from_buffer_correctly_parses_surface_to_surface_pdu() { + let buffer = SURFACE_TO_SURFACE_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*SURFACE_TO_SURFACE, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_surface_to_surface_pdu() { + let buffer = encode_vec(&*SURFACE_TO_SURFACE).unwrap(); + + assert_eq!(buffer, SURFACE_TO_SURFACE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_surface_to_surface_pdu() { + assert_eq!(SURFACE_TO_SURFACE_BUFFER.len(), SURFACE_TO_SURFACE.size()); +} + +#[test] +fn from_buffer_correctly_parses_surface_to_cache_pdu() { + let buffer = SURFACE_TO_CACHE_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*SURFACE_TO_CACHE, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_surface_to_cache_pdu() { + let buffer = encode_vec(&*SURFACE_TO_CACHE).unwrap(); + + assert_eq!(buffer, SURFACE_TO_CACHE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_surface_to_cache_pdu() { + assert_eq!(SURFACE_TO_CACHE_BUFFER.len(), SURFACE_TO_CACHE.size()); +} + +#[test] +fn from_buffer_correctly_parses_cache_to_surface_pdu() { + let buffer = CACHE_TO_SURFACE_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*CACHE_TO_SURFACE, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_cache_to_surface_pdu() { + let buffer = encode_vec(&*CACHE_TO_SURFACE).unwrap(); + + assert_eq!(buffer, CACHE_TO_SURFACE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_cache_to_surface_pdu() { + assert_eq!(CACHE_TO_SURFACE_BUFFER.len(), CACHE_TO_SURFACE.size()); +} + +#[test] +fn from_buffer_correctly_parses_create_surface_pdu() { + let buffer = CREATE_SURFACE_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*CREATE_SURFACE, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_create_surface_pdu() { + let buffer = encode_vec(&*CREATE_SURFACE).unwrap(); + + assert_eq!(buffer, CREATE_SURFACE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_create_surface_pdu() { + assert_eq!(CREATE_SURFACE_BUFFER.len(), CREATE_SURFACE.size()); +} + +#[test] +fn from_buffer_correctly_parses_delete_surface_pdu() { + let buffer = DELETE_SURFACE_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*DELETE_SURFACE, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_delete_surface_pdu() { + let buffer = encode_vec(&*DELETE_SURFACE).unwrap(); + + assert_eq!(buffer, DELETE_SURFACE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_delete_surface_pdu() { + assert_eq!(DELETE_SURFACE_BUFFER.len(), DELETE_SURFACE.size()); +} + +#[test] +fn from_buffer_correctly_parses_reset_graphics() { + let buffer = RESET_GRAPHICS_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*RESET_GRAPHICS, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_reset_graphics() { + let buffer = encode_vec(&*RESET_GRAPHICS).unwrap(); + + assert_eq!(buffer, RESET_GRAPHICS_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_reset_graphics() { + assert_eq!(RESET_GRAPHICS_BUFFER.len(), RESET_GRAPHICS.size()); +} + +#[test] +fn from_buffer_correctly_parses_map_surface_to_output_pdu() { + let buffer = MAP_SURFACE_TO_OUTPUT_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*MAP_SURFACE_TO_OUTPUT, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_map_surface_to_output_pdu() { + let buffer = encode_vec(&*MAP_SURFACE_TO_OUTPUT).unwrap(); + + assert_eq!(buffer, MAP_SURFACE_TO_OUTPUT_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_map_surface_to_output_pdu() { + assert_eq!(MAP_SURFACE_TO_OUTPUT_BUFFER.len(), MAP_SURFACE_TO_OUTPUT.size()); +} + +#[test] +fn from_buffer_correctly_parses_evict_cache_entry_pdu() { + let buffer = EVICT_CACHE_ENTRY_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*EVICT_CACHE_ENTRY, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_evict_cache_entry_pdu() { + let buffer = encode_vec(&*EVICT_CACHE_ENTRY).unwrap(); + + assert_eq!(buffer, EVICT_CACHE_ENTRY_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_evict_cache_entry_pdu() { + assert_eq!(EVICT_CACHE_ENTRY_BUFFER.len(), EVICT_CACHE_ENTRY.size()); +} + +#[test] +fn from_buffer_correctly_parses_start_frame_pdu() { + let buffer = START_FRAME_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*START_FRAME, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_start_frame_pdu() { + let buffer = encode_vec(&*START_FRAME).unwrap(); + + assert_eq!(buffer, START_FRAME_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_start_frame_pdu() { + assert_eq!(START_FRAME_BUFFER.len(), START_FRAME.size()); +} + +#[test] +fn from_buffer_correctly_parses_end_frame_pdu() { + let buffer = END_FRAME_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*END_FRAME, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_end_frame_pdu() { + let buffer = encode_vec(&*END_FRAME).unwrap(); + + assert_eq!(buffer, END_FRAME_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_end_frame_pdu() { + assert_eq!(END_FRAME_BUFFER.len(), END_FRAME.size()); +} + +#[test] +fn from_buffer_correctly_parses_capabilities_confirm_pdu() { + let buffer = CAPABILITIES_CONFIRM_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*CAPABILITIES_CONFIRM, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_capabilities_confirm_pdu() { + let buffer = encode_vec(&*CAPABILITIES_CONFIRM).unwrap(); + + assert_eq!(buffer, CAPABILITIES_CONFIRM_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_capabilities_confirm_pdu() { + assert_eq!(CAPABILITIES_CONFIRM_BUFFER.len(), CAPABILITIES_CONFIRM.size()); +} + +#[test] +fn from_buffer_correctly_parses_capabilities_advertise_pdu() { + let buffer = CAPABILITIES_ADVERTISE_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*CAPABILITIES_ADVERTISE, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_capabilities_advertise_pdu() { + let buffer = encode_vec(&*CAPABILITIES_ADVERTISE).unwrap(); + + assert_eq!(buffer, CAPABILITIES_ADVERTISE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_capabilities_advertise_pdu() { + assert_eq!(CAPABILITIES_ADVERTISE_BUFFER.len(), CAPABILITIES_ADVERTISE.size()); +} + +#[test] +fn from_buffer_correctly_parses_frame_acknowledge_pdu() { + let buffer = FRAME_ACKNOWLEDGE_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*FRAME_ACKNOWLEDGE, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_frame_acknowledge_pdu() { + let buffer = encode_vec(&*FRAME_ACKNOWLEDGE).unwrap(); + + assert_eq!(buffer, FRAME_ACKNOWLEDGE_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_frame_acknowledge_pdu() { + assert_eq!(FRAME_ACKNOWLEDGE_BUFFER.len(), FRAME_ACKNOWLEDGE.size()); +} + +#[test] +fn from_buffer_correctly_parses_cache_import_reply() { + let buffer = CACHE_IMPORT_REPLY_BUFFER.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*CACHE_IMPORT_REPLY, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_cache_import_reply() { + let buffer = encode_vec(&*CACHE_IMPORT_REPLY).unwrap(); + + assert_eq!(buffer, CACHE_IMPORT_REPLY_BUFFER.as_ref()); +} + +#[test] +fn buffer_length_is_correct_for_cache_import_reply() { + assert_eq!(CACHE_IMPORT_REPLY_BUFFER.len(), CACHE_IMPORT_REPLY.size()); +} + +#[test] +fn from_buffer_consume_correctly_parses_incorrect_len_avc_444_message() { + let buffer = AVC_444_MESSAGE_INCORRECT_LEN.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*AVC_444_BITMAP, decode_cursor(&mut cursor).unwrap()); + assert!(!cursor.is_empty()); +} + +#[test] +fn from_buffer_consume_correctly_parses_avc_444_message() { + let buffer = AVC_444_MESSAGE_CORRECT_LEN.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*AVC_444_BITMAP, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_consume_correctly_serializes_avc_444_message() { + let buffer = encode_vec(&*AVC_444_BITMAP).unwrap(); + let expected = AVC_444_MESSAGE_CORRECT_LEN.as_ref(); + + assert_eq!(expected, buffer.as_slice()); +} diff --git a/crates/ironrdp-testsuite-core/tests/pdu/input.rs b/crates/ironrdp-testsuite-core/tests/pdu/input.rs new file mode 100644 index 00000000..7b31672c --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/input.rs @@ -0,0 +1,70 @@ +use std::sync::LazyLock; + +use ironrdp_core::{decode_cursor, encode_vec, ReadCursor}; +use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent}; +use ironrdp_pdu::input::mouse::PointerFlags; +use ironrdp_pdu::input::MousePdu; + +const FASTPATH_INPUT_MESSAGE: [u8; 44] = [ + 0x18, 0x2c, 0x20, 0x0, 0x90, 0x1a, 0x0, 0x26, 0x4, 0x20, 0x0, 0x8, 0x1b, 0x0, 0x26, 0x4, 0x20, 0x0, 0x10, 0x1b, + 0x0, 0x26, 0x4, 0x20, 0x0, 0x8, 0x1a, 0x0, 0x27, 0x4, 0x20, 0x0, 0x8, 0x19, 0x0, 0x27, 0x4, 0x20, 0x0, 0x8, 0x19, + 0x0, 0x28, 0x4, +]; + +static FASTPATH_INPUT: LazyLock = LazyLock::new(|| { + FastPathInput::new(vec![ + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::DOWN | PointerFlags::LEFT_BUTTON, + number_of_wheel_rotation_units: 0, + x_position: 26, + y_position: 1062, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::MOVE, + number_of_wheel_rotation_units: 0, + x_position: 27, + y_position: 1062, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::LEFT_BUTTON, + number_of_wheel_rotation_units: 0, + x_position: 27, + y_position: 1062, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::MOVE, + number_of_wheel_rotation_units: 0, + x_position: 26, + y_position: 1063, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::MOVE, + number_of_wheel_rotation_units: 0, + x_position: 25, + y_position: 1063, + }), + FastPathInputEvent::MouseEvent(MousePdu { + flags: PointerFlags::MOVE, + number_of_wheel_rotation_units: 0, + x_position: 25, + y_position: 1064, + }), + ]) + .expect("can't panic") +}); + +#[test] +fn from_buffer_correctly_parses_fastpath_input_message() { + let buffer = FASTPATH_INPUT_MESSAGE.as_ref(); + + let mut cursor = ReadCursor::new(buffer); + assert_eq!(*FASTPATH_INPUT, decode_cursor(&mut cursor).unwrap()); + assert!(cursor.is_empty()); +} + +#[test] +fn to_buffer_correctly_serializes_fastpath_input_message() { + let buffer = encode_vec(&*FASTPATH_INPUT).unwrap(); + + assert_eq!(buffer, FASTPATH_INPUT_MESSAGE.as_ref()); +} diff --git a/crates/ironrdp-testsuite-core/tests/pdu/mcs.rs b/crates/ironrdp-testsuite-core/tests/pdu/mcs.rs new file mode 100644 index 00000000..fc2a3c37 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/mcs.rs @@ -0,0 +1,76 @@ +use expect_test::expect; +use ironrdp_core::{decode, encode_vec, Encode as _}; +use ironrdp_pdu::mcs::*; +use ironrdp_testsuite_core::mcs::*; +use ironrdp_testsuite_core::mcs_encode_decode_test; + +fn mcs_decode<'de, T: McsPdu<'de>>(src: &'de [u8]) -> ironrdp_core::DecodeResult { + let mut cursor = ironrdp_core::ReadCursor::new(src); + T::mcs_body_decode(&mut cursor, src.len()) +} + +#[test] +fn invalid_domain_mcspdu() { + let e = mcs_decode::>(&[0x48, 0x00, 0x00, 0x00, 0x70, 0x00, 0x01, 0x03, 0xEB, 0x70, 0x14]) + .err() + .unwrap(); + + expect![[r#" + Error { + context: "McsMessage", + kind: InvalidField { + field: "domain-mcspdu", + reason: "unexpected application tag for CHOICE", + }, + source: None, + } + "#]] + .assert_debug_eq(&e); +} + +mcs_encode_decode_test! { + erect_domain_request: ERECT_DOMAIN_PDU, ERECT_DOMAIN_PDU_BUFFER; + attach_user_request: ATTACH_USER_REQUEST_PDU, ATTACH_USER_REQUEST_PDU_BUFFER; + attach_user_confirm: ATTACH_USER_CONFIRM_PDU, ATTACH_USER_CONFIRM_PDU_BUFFER; + channel_join_request: CHANNEL_JOIN_REQUEST_PDU, CHANNEL_JOIN_REQUEST_PDU_BUFFER; + channel_join_confirm: CHANNEL_JOIN_CONFIRM_PDU, CHANNEL_JOIN_CONFIRM_PDU_BUFFER; + send_data_request: SEND_DATA_REQUEST_PDU, SEND_DATA_REQUEST_PDU_BUFFER; + send_data_indication: SEND_DATA_INDICATION_PDU, SEND_DATA_INDICATION_PDU_BUFFER; + disconnect_ultimatum: DISCONNECT_PROVIDER_ULTIMATUM_PDU, DISCONNECT_PROVIDER_ULTIMATUM_PDU_BUFFER; +} + +#[test] +fn from_buffer_correct_parses_connect_initial() { + let blocks: ConnectInitial = decode(CONNECT_INITIAL_BUFFER.as_slice()).unwrap(); + assert_eq!(blocks, *CONNECT_INITIAL); +} + +#[test] +fn to_buffer_correct_serializes_connect_initial() { + let buf = encode_vec(&*CONNECT_INITIAL).unwrap(); + assert_eq!(buf, CONNECT_INITIAL_BUFFER); +} + +#[test] +fn buffer_length_is_correct_for_connect_initial() { + let len = CONNECT_INITIAL.size(); + assert_eq!(len, CONNECT_INITIAL_BUFFER.len()); +} + +#[test] +fn from_buffer_correct_parses_connect_response() { + let blocks: ConnectResponse = decode(CONNECT_RESPONSE_BUFFER.as_slice()).unwrap(); + assert_eq!(blocks, *CONNECT_RESPONSE); +} + +#[test] +fn to_buffer_correct_serializes_connect_response() { + let buf = encode_vec(&*CONNECT_RESPONSE).unwrap(); + assert_eq!(buf, CONNECT_RESPONSE_BUFFER); +} + +#[test] +fn buffer_length_is_correct_for_connect_response() { + let len = CONNECT_RESPONSE.size(); + assert_eq!(len, CONNECT_RESPONSE_BUFFER.len()); +} diff --git a/crates/ironrdp-testsuite-core/tests/pdu/mod.rs b/crates/ironrdp-testsuite-core/tests/pdu/mod.rs new file mode 100644 index 00000000..7f0610d2 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/mod.rs @@ -0,0 +1,12 @@ +mod gcc; +mod gfx; +mod input; +mod mcs; +#[expect( + clippy::needless_raw_strings, + reason = "the lint is disable to not interfere with expect! macro" +)] +mod pointer; +mod rdp; +mod rfx; +mod x224; diff --git a/crates/ironrdp-testsuite-core/tests/pdu/pointer/mod.rs b/crates/ironrdp-testsuite-core/tests/pdu/pointer/mod.rs new file mode 100644 index 00000000..5c0d0216 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/pointer/mod.rs @@ -0,0 +1,202 @@ +use std::io::Cursor; + +use expect_test::expect; +use ironrdp_graphics::pointer::{DecodedPointer, PointerBitmapTarget}; +use ironrdp_pdu::pointer::{ + CachedPointerAttribute, ColorPointerAttribute, LargePointerAttribute, Point16, PointerAttribute, + PointerPositionAttribute, +}; + +fn expect_pointer_png(pointer: &DecodedPointer, expected_file_path: &str) { + let path = format!("{}/test_data/{}", env!("CARGO_MANIFEST_DIR"), expected_file_path); + + if std::env::var("UPDATE_EXPECT").unwrap_or_default() == "1" { + let mut encoded_png = Vec::new(); + + let mut png = png::Encoder::new(&mut encoded_png, u32::from(pointer.width), u32::from(pointer.height)); + png.set_color(png::ColorType::Rgba); + png.set_depth(png::BitDepth::Eight); + png.write_header() + .unwrap() + .write_image_data(&pointer.bitmap_data) + .unwrap(); + std::fs::write(path, &encoded_png).unwrap(); + return; + } + + if !std::path::Path::new(&path).exists() { + panic!("Test file {path} does not exist"); + } + + let png_buffer = std::fs::read(path).unwrap(); + let mut png_reader = png::Decoder::new(Cursor::new(&png_buffer)).read_info().unwrap(); + let mut png_reader_buffer = vec![0u8; png_reader.output_buffer_size().unwrap()]; + let frame_size = png_reader.next_frame(&mut png_reader_buffer).unwrap().buffer_size(); + let expected = &png_reader_buffer[..frame_size]; + assert_eq!(expected, &pointer.bitmap_data); +} + +#[test] +fn new_pointer_32bpp() { + let data = include_bytes!("../../../test_data/pdu/pointer/new_pointer_32bpp.bin"); + let mut parsed = ironrdp_core::decode::>(data).unwrap(); + let decoded = DecodedPointer::decode_pointer_attribute(&parsed, PointerBitmapTarget::Software).unwrap(); + expect_pointer_png(&decoded, "pdu/pointer/new_pointer_32bpp.png"); + + let encoded = ironrdp_core::encode_vec(&parsed).unwrap(); + assert_eq!(&encoded, data); + + parsed.color_pointer.and_mask = &[]; + parsed.color_pointer.xor_mask = &[]; + expect![[r#" + PointerAttribute { + xor_bpp: 32, + color_pointer: ColorPointerAttribute { + cache_index: 0, + hot_spot: Point16 { + x: 3, + y: 3, + }, + width: 41, + height: 39, + xor_mask: [], + and_mask: [], + }, + } + "#]] + .assert_debug_eq(&parsed); +} + +#[test] +fn large_pointer_32bpp() { + let data = include_bytes!("../../../test_data/pdu/pointer/large_pointer_32bpp.bin"); + let mut parsed = ironrdp_core::decode::>(data).unwrap(); + let decoded = DecodedPointer::decode_large_pointer_attribute(&parsed, PointerBitmapTarget::Software).unwrap(); + expect_pointer_png(&decoded, "pdu/pointer/large_pointer_32bpp.png"); + + let encoded = ironrdp_core::encode_vec(&parsed).unwrap(); + assert_eq!(&encoded, data); + + parsed.and_mask = &[]; + parsed.xor_mask = &[]; + expect![[r#" + LargePointerAttribute { + xor_bpp: 32, + cache_index: 12, + hot_spot: Point16 { + x: 2, + y: 0, + }, + width: 112, + height: 112, + xor_mask: [], + and_mask: [], + } + "#]] + .assert_debug_eq(&parsed); +} + +#[test] +fn color_pointer_24bpp() { + let data = include_bytes!("../../../test_data/pdu/pointer/color_pointer_24bpp.bin"); + let mut parsed = ironrdp_core::decode::>(data).unwrap(); + let decoded = DecodedPointer::decode_color_pointer_attribute(&parsed, PointerBitmapTarget::Software).unwrap(); + expect_pointer_png(&decoded, "pdu/pointer/color_pointer_24bpp.png"); + + let encoded = ironrdp_core::encode_vec(&parsed).unwrap(); + assert_eq!(&encoded, data); + + parsed.and_mask = &[]; + parsed.xor_mask = &[]; + expect![[r#" + ColorPointerAttribute { + cache_index: 0, + hot_spot: Point16 { + x: 3, + y: 11, + }, + width: 41, + height: 39, + xor_mask: [], + and_mask: [], + } + "#]] + .assert_debug_eq(&parsed); +} + +#[test] +fn color_pointer_1bpp() { + // Hand-crafted cursor with transparent, black and inverted pixels + const AND_MASK_1BPP: &[u8] = &[ + 0b01111110, 0b00000000, 0b10011110, 0b00000000, 0b10000110, 0b00000000, 0b11000010, 0b00000000, 0b11000110, + 0b00000000, 0b11101010, 0b00000000, 0b11111100, 0b00000000, + ]; + + const XOR_MASK_1BPP: &[u8] = &[ + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00010000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + ]; + + let value = PointerAttribute { + xor_bpp: 1, + color_pointer: ColorPointerAttribute { + cache_index: 0, + hot_spot: Point16 { x: 0, y: 0 }, + width: 7, + height: 7, + xor_mask: XOR_MASK_1BPP, + and_mask: AND_MASK_1BPP, + }, + }; + + // Re-encode test + let encoded = ironrdp_core::encode_vec(&value).unwrap(); + let decoded = ironrdp_core::decode::>(&encoded).unwrap(); + assert_eq!(&decoded, &value); + + let decoded = DecodedPointer::decode_pointer_attribute(&value, PointerBitmapTarget::Software).unwrap(); + expect_pointer_png(&decoded, "pdu/pointer/color_pointer_1bpp.png"); +} + +#[test] +fn color_pointer_16bpp() { + const AND_MASK_16BPP: &[u8] = &[0b10111110, 0b00000000, 0b01111110, 0b00000000]; + + const XOR_MASK_16BPP: &[u8] = &[0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF]; + + let value = PointerAttribute { + xor_bpp: 16, + color_pointer: ColorPointerAttribute { + cache_index: 0, + hot_spot: Point16 { x: 0, y: 0 }, + width: 2, + height: 2, + xor_mask: XOR_MASK_16BPP, + and_mask: AND_MASK_16BPP, + }, + }; + + // Re-encode test + let encoded = ironrdp_core::encode_vec(&value).unwrap(); + let decoded = ironrdp_core::decode::>(&encoded).unwrap(); + assert_eq!(&decoded, &value); + + let decoded = DecodedPointer::decode_pointer_attribute(&value, PointerBitmapTarget::Software).unwrap(); + expect_pointer_png(&decoded, "pdu/pointer/color_pointer_16bpp.png"); +} + +#[test] +fn cached_pointer() { + let value = CachedPointerAttribute { cache_index: 42 }; + let encoded = ironrdp_core::encode_vec(&value).unwrap(); + let decoded = ironrdp_core::decode::(&encoded).unwrap(); + assert_eq!(&decoded, &value); +} + +#[test] +fn set_pointer_position() { + let value = PointerPositionAttribute { x: 12, y: 34 }; + let encoded = ironrdp_core::encode_vec(&value).unwrap(); + let decoded = ironrdp_core::decode::(&encoded).unwrap(); + assert_eq!(&decoded, &value); +} diff --git a/crates/ironrdp-testsuite-core/tests/pdu/rdp.rs b/crates/ironrdp-testsuite-core/tests/pdu/rdp.rs new file mode 100644 index 00000000..f9fc45df --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/rdp.rs @@ -0,0 +1,449 @@ +use ironrdp_core::{decode, encode_vec, Encode as _}; +use ironrdp_testsuite_core::capsets::*; +use ironrdp_testsuite_core::client_info::*; +use ironrdp_testsuite_core::rdp::*; + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_client_info() { + let buf = CLIENT_INFO_PDU_BUFFER; + + assert_eq!(CLIENT_INFO_PDU.clone(), decode(buf.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_server_license() { + assert_eq!(*SERVER_LICENSE_PDU, decode(&SERVER_LICENSE_BUFFER).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_server_demand_active() { + let buf = SERVER_DEMAND_ACTIVE_PDU_BUFFER; + + assert_eq!(SERVER_DEMAND_ACTIVE_PDU.clone(), decode(buf.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_client_demand_active() { + let buf = CLIENT_DEMAND_ACTIVE_PDU_BUFFER; + + assert_eq!(CLIENT_DEMAND_ACTIVE_PDU.clone(), decode(buf.as_slice()).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_client_synchronize() { + let buf = CLIENT_SYNCHRONIZE_BUFFER.as_ref(); + + assert_eq!(CLIENT_SYNCHRONIZE.clone(), decode(buf).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_client_control_cooperate() { + let buf = CONTROL_COOPERATE_BUFFER.as_ref(); + + assert_eq!(CONTROL_COOPERATE.clone(), decode(buf).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_client_control_request_control() { + let buf = CONTROL_REQUEST_CONTROL_BUFFER.as_ref(); + + assert_eq!(CONTROL_REQUEST_CONTROL.clone(), decode(buf).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_server_control_granted_control() { + let buf = SERVER_GRANTED_CONTROL_BUFFER.as_ref(); + + assert_eq!(SERVER_GRANTED_CONTROL.clone(), decode(buf).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_client_font_list() { + let buf = CLIENT_FONT_LIST_BUFFER.as_ref(); + + assert_eq!(CLIENT_FONT_LIST.clone(), decode(buf).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_server_font_map() { + let buf = SERVER_FONT_MAP_BUFFER.as_ref(); + + assert_eq!(SERVER_FONT_MAP.clone(), decode(buf).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_rdp_pdu_server_monitor_layout() { + let buf = MONITOR_LAYOUT_PDU_BUFFER.clone(); + + assert_eq!(MONITOR_LAYOUT_PDU.clone(), decode(buf.as_slice()).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_client_info() { + let buf = encode_vec(&*CLIENT_INFO_PDU).unwrap(); + assert_eq!(buf, CLIENT_INFO_PDU_BUFFER.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_server_license() { + let buf = encode_vec(&*SERVER_LICENSE_PDU).unwrap(); + + assert_eq!(SERVER_LICENSE_BUFFER.as_ref(), buf.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_server_demand_active() { + let buf = encode_vec(&*SERVER_DEMAND_ACTIVE_PDU).unwrap(); + assert_eq!(buf, SERVER_DEMAND_ACTIVE_PDU_BUFFER.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_client_demand_active() { + let buf = encode_vec(&*CLIENT_DEMAND_ACTIVE_PDU).unwrap(); + assert_eq!(buf, CLIENT_DEMAND_ACTIVE_PDU_BUFFER.as_slice()); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_client_synchronize() { + let pdu = CLIENT_SYNCHRONIZE.clone(); + let expected_buf = CLIENT_SYNCHRONIZE_BUFFER.to_vec(); + + let buf = encode_vec(&pdu).unwrap(); + + assert_eq!(expected_buf, buf); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_client_control_cooperate() { + let pdu = CONTROL_COOPERATE.clone(); + let expected_buf = CONTROL_COOPERATE_BUFFER.to_vec(); + + let buf = encode_vec(&pdu).unwrap(); + + assert_eq!(expected_buf, buf); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_client_control_request_control() { + let pdu = CONTROL_REQUEST_CONTROL.clone(); + let expected_buf = CONTROL_REQUEST_CONTROL_BUFFER.to_vec(); + + let buf = encode_vec(&pdu).unwrap(); + + assert_eq!(expected_buf, buf); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_server_control_granted_control() { + let pdu = SERVER_GRANTED_CONTROL.clone(); + let expected_buf = SERVER_GRANTED_CONTROL_BUFFER.to_vec(); + + let buf = encode_vec(&pdu).unwrap(); + + assert_eq!(expected_buf, buf); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_client_font_list() { + let pdu = CLIENT_FONT_LIST.clone(); + let expected_buf = CLIENT_FONT_LIST_BUFFER.to_vec(); + + let buf = encode_vec(&pdu).unwrap(); + + assert_eq!(expected_buf, buf); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_server_font_map() { + let pdu = SERVER_FONT_MAP.clone(); + let expected_buf = SERVER_FONT_MAP_BUFFER.to_vec(); + + let buf = encode_vec(&pdu).unwrap(); + + assert_eq!(expected_buf, buf); +} + +#[test] +fn to_buffer_correctly_serializes_rdp_pdu_server_monitor_layout() { + let pdu = MONITOR_LAYOUT_PDU.clone(); + let expected_buf = MONITOR_LAYOUT_PDU_BUFFER.to_vec(); + + let buf = encode_vec(&pdu).unwrap(); + + assert_eq!(expected_buf, buf); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_client_info() { + let pdu = CLIENT_INFO_PDU.clone(); + let expected_buf_len = CLIENT_INFO_PDU_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_server_license() { + let len = SERVER_LICENSE_PDU.size(); + + assert_eq!(SERVER_LICENSE_BUFFER.len(), len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_server_demand_active() { + let pdu = SERVER_DEMAND_ACTIVE_PDU.clone(); + let expected_buf_len = SERVER_DEMAND_ACTIVE_PDU_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_client_demand_active() { + let pdu = CLIENT_DEMAND_ACTIVE_PDU.clone(); + let expected_buf_len = CLIENT_DEMAND_ACTIVE_PDU_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_client_synchronize() { + let pdu = CLIENT_SYNCHRONIZE.clone(); + let expected_buf_len = CLIENT_SYNCHRONIZE_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_client_control_cooperate() { + let pdu = CONTROL_COOPERATE.clone(); + let expected_buf_len = CONTROL_COOPERATE_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_client_control_request_control() { + let pdu = CONTROL_REQUEST_CONTROL.clone(); + let expected_buf_len = CONTROL_REQUEST_CONTROL_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_server_control_granted_control() { + let pdu = SERVER_GRANTED_CONTROL.clone(); + let expected_buf_len = SERVER_GRANTED_CONTROL_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_client_font_list() { + let pdu = CLIENT_FONT_LIST.clone(); + let expected_buf_len = CLIENT_FONT_LIST_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_server_font_map() { + let pdu = SERVER_FONT_MAP.clone(); + let expected_buf_len = SERVER_FONT_MAP_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn buffer_length_is_correct_for_rdp_pdu_server_monitor_layout() { + let pdu = MONITOR_LAYOUT_PDU.clone(); + let expected_buf_len = MONITOR_LAYOUT_PDU_BUFFER.len(); + + let len = pdu.size(); + + assert_eq!(expected_buf_len, len); +} + +#[test] +fn from_buffer_correct_parses_client_info_pdu_ansi() { + assert_eq!( + CLIENT_INFO_ANSI.clone(), + decode(CLIENT_INFO_BUFFER_ANSI.as_ref()).unwrap() + ); +} + +#[test] +fn from_buffer_correct_parses_client_info_pdu_unicode() { + assert_eq!( + CLIENT_INFO_UNICODE.clone(), + decode(CLIENT_INFO_BUFFER_UNICODE.as_ref()).unwrap() + ); +} + +#[test] +fn from_buffer_correct_parses_client_info_pdu_unicode_without_optional_fields() { + assert_eq!( + CLIENT_INFO_UNICODE_WITHOUT_OPTIONAL_FIELDS.clone(), + decode(CLIENT_INFO_BUFFER_UNICODE_WITHOUT_OPTIONAL_FIELDS.as_slice()).unwrap() + ); +} + +#[test] +fn to_buffer_correct_serializes_client_info_pdu_ansi() { + let data = CLIENT_INFO_ANSI.clone(); + let expected_buffer = CLIENT_INFO_BUFFER_ANSI.to_vec(); + + let buffer = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer, buffer); +} + +#[test] +fn buffer_length_is_correct_for_client_info_pdu_ansi() { + let data = CLIENT_INFO_ANSI.clone(); + let expected_buffer_len = CLIENT_INFO_BUFFER_ANSI.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn to_buffer_correct_serializes_client_info_pdu_unicode() { + let data = CLIENT_INFO_UNICODE.clone(); + let expected_buffer = CLIENT_INFO_BUFFER_UNICODE.to_vec(); + + let buffer = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer, buffer); +} + +#[test] +fn buffer_length_is_correct_for_client_info_pdu_unicode() { + let data = CLIENT_INFO_UNICODE.clone(); + let expected_buffer_len = CLIENT_INFO_BUFFER_UNICODE.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn to_buffer_correct_serializes_client_info_pdu_unicode_without_optional_fields() { + let data = CLIENT_INFO_UNICODE_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer = CLIENT_INFO_BUFFER_UNICODE_WITHOUT_OPTIONAL_FIELDS.to_vec(); + + let buffer = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer, buffer); +} + +#[test] +fn buffer_length_is_correct_for_client_info_pdu_unicode_without_optional_fields() { + let data = CLIENT_INFO_UNICODE_WITHOUT_OPTIONAL_FIELDS.clone(); + let expected_buffer_len = CLIENT_INFO_BUFFER_UNICODE_WITHOUT_OPTIONAL_FIELDS.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn from_buffer_correctly_parses_server_demand_active() { + let buffer = SERVER_DEMAND_ACTIVE_BUFFER.as_ref(); + + assert_eq!(*SERVER_DEMAND_ACTIVE, decode(buffer).unwrap()); +} + +#[test] +fn from_buffer_correctly_parses_client_demand_active_with_incomplete_capability_set() { + let buffer = CLIENT_DEMAND_ACTIVE_WITH_INCOMPLETE_CAPABILITY_SET_BUFFER.as_ref(); + + assert_eq!( + *CLIENT_DEMAND_ACTIVE_WITH_INCOMPLETE_CAPABILITY_SET, + decode(buffer).unwrap() + ); +} + +#[test] +fn from_buffer_correctly_parses_client_demand_active() { + let buffer = CLIENT_DEMAND_ACTIVE_BUFFER.as_ref(); + + assert_eq!(*CLIENT_DEMAND_ACTIVE, decode(buffer).unwrap()); +} + +#[test] +fn to_buffer_correctly_serializes_server_demand_active() { + let data = SERVER_DEMAND_ACTIVE.clone(); + let expected_buffer = SERVER_DEMAND_ACTIVE_BUFFER.to_vec(); + + let buff = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer, buff); +} + +#[test] +fn to_buffer_correctly_serializes_client_demand_active_with_incomplete_capability_set() { + let data = CLIENT_DEMAND_ACTIVE_WITH_INCOMPLETE_CAPABILITY_SET.clone(); + let expected_buffer = CLIENT_DEMAND_ACTIVE_WITH_INCOMPLETE_CAPABILITY_SET_BUFFER.to_vec(); + + let buff = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer, buff); +} + +#[test] +fn to_buffer_correctly_serializes_client_demand_active() { + let data = CLIENT_DEMAND_ACTIVE.clone(); + let expected_buffer = CLIENT_DEMAND_ACTIVE_BUFFER.to_vec(); + + let buff = encode_vec(&data).unwrap(); + + assert_eq!(expected_buffer, buff); +} + +#[test] +fn buffer_length_is_correct_for_server_demand_active() { + let data = SERVER_DEMAND_ACTIVE.clone(); + let expected_buffer_len = SERVER_DEMAND_ACTIVE_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_client_demand_active_with_incomplete_capability_set() { + let data = CLIENT_DEMAND_ACTIVE_WITH_INCOMPLETE_CAPABILITY_SET.clone(); + let expected_buffer_len = CLIENT_DEMAND_ACTIVE_WITH_INCOMPLETE_CAPABILITY_SET_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} + +#[test] +fn buffer_length_is_correct_for_client_demand_active() { + let data = CLIENT_DEMAND_ACTIVE.clone(); + let expected_buffer_len = CLIENT_DEMAND_ACTIVE_BUFFER.len(); + + let len = data.size(); + + assert_eq!(expected_buffer_len, len); +} diff --git a/crates/ironrdp-testsuite-core/tests/pdu/rfx.rs b/crates/ironrdp-testsuite-core/tests/pdu/rfx.rs new file mode 100644 index 00000000..c6221933 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/rfx.rs @@ -0,0 +1,377 @@ +use std::sync::LazyLock; + +use ironrdp_pdu::codecs::rfx::*; +use ironrdp_pdu::decode; +use ironrdp_testsuite_core::encode_decode_test; + +const SYNC_PDU_BUFFER: [u8; 12] = [ + 0xc0, 0xcc, // TS_RFX_SYNC::BlockT::blockType = WBT_SYNC + 0x0c, 0x00, 0x00, 0x00, // TS_RFX_SYNC::BlockT::blockLen = 12 + 0xca, 0xac, 0xcc, 0xca, // TS_RFX_SYNC::magic = WF_MAGIC + 0x00, 0x01, // TS_RFX_SYNC::version = 0x0100 +]; + +const SYNC_PDU_BUFFER_WITH_ZERO_DATA_LENGTH: [u8; 12] = [ + 0xc0, 0xcc, // TS_RFX_SYNC::BlockT::blockType = WBT_SYNC + 0x00, 0x00, 0x00, 0x00, // TS_RFX_SYNC::BlockT::blockLen = 0 + 0xca, 0xac, 0xcc, 0xca, // TS_RFX_SYNC::magic = WF_MAGIC + 0x00, 0x01, // TS_RFX_SYNC::version = 0x0100 +]; + +const SYNC_PDU_BUFFER_WITH_BIG_DATA_LENGTH: [u8; 12] = [ + 0xc0, 0xcc, // TS_RFX_SYNC::BlockT::blockType = WBT_SYNC + 0xff, 0x00, 0x00, 0x00, // TS_RFX_SYNC::BlockT::blockLen = 0xff + 0xca, 0xac, 0xcc, 0xca, // TS_RFX_SYNC::magic = WF_MAGIC + 0x00, 0x01, // TS_RFX_SYNC::version = 0x0100 +]; + +const SYNC_PDU_BUFFER_WITH_SMALL_BUFFER: [u8; 10] = [ + 0xc6, 0xcc, // TS_RFX_SYNC::BlockT::blockType = WBT_REGION + 0x0c, 0x00, 0x00, 0x00, // TS_RFX_SYNC::BlockT::blockLen = 0x0c + 0x01, 0x00, 0x00, 0x00, +]; + +const CODEC_VERSIONS_PDU_BUFFER: [u8; 10] = [ + 0xc1, 0xcc, // TS_RFX_CODEC_VERSIONS::BlockT::blockType = WBT_CODEC_VERSION + 0x0a, 0x00, 0x00, 0x00, // TS_RFX_CODEC_VERSIONS::BlockT::blockLen = 10 + 0x01, // TS_RFX_CODEC_VERSIONS::numCodecs = 1 + 0x01, // TS_RFX_CODEC_VERSIONS::TS_RFX_CODEC_VERSIONT::codecId = 1 + 0x00, 0x01, // TS_RFX_CODEC_VERSIONS::TS_RFX_CODEC_VERSIONT::version 0x0100 +]; + +const CHANNELS_PDU_BUFFER: [u8; 17] = [ + 0xc2, 0xcc, // TS_RFX_CHANNELS::BLockT::blockType = WBT_CHANNELS + 0x11, 0x00, 0x00, 0x00, // TS_RFX_CHANNELS::BlockT::blockLen = 17 + 0x02, // TS_RFX_CHANNELS::numChannels = 2 + 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::channelId = 0 + 0x40, 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::width = 64 + 0x40, 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::height = 64 + 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::channelId = 0 + 0x20, 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::width = 32 + 0x20, 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::height = 32 +]; + +const CHANNELS_PDU_BUFFER_WITH_INVALID_DATA_LENGTH: [u8; 17] = [ + 0xc2, 0xcc, // TS_RFX_CHANNELS::BLockT::blockType = WBT_CHANNELS + 0x11, 0x00, 0x00, 0x00, // TS_RFX_CHANNELS::BlockT::blockLen = 17 + 0x0a, // TS_RFX_CHANNELS::numChannels = 0x0a + 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::channelId = 0 + 0x40, 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::width = 64 + 0x40, 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::height = 64 + 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::channelId = 0 + 0x20, 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::width = 32 + 0x20, 0x00, // TS_RFX_CHANNELS::TS_RFX_CHANNELT::height = 32 +]; + +const CONTEXT_PDU_BUFFER: [u8; 13] = [ + 0xc3, 0xcc, // TS_RFX_CONTEXT::CodecChannelT::BlockT::blockType = WBT_CONTEXT + 0x0d, 0x00, 0x00, 0x00, // TS_RFX_CONTEXT::CodecChannelT::BlockT::blockLen = 13 + 0x01, // TS_RFX_CONTEXT::CodecChannelT::codecId = 1 + 0xff, // TS_RFX_CONTEXT::CodecChannelT::channelId = 255 + 0x00, // TS_RFX_CONTEXT::ctxId = 0 + 0x40, 0x00, // TS_RFX_CONTEXT::tileSize = 64 + 0x28, + 0x28, // TS_RFX_CONTEXT::properties + // TS_RFX_CONTEXT::properties::flags = VIDEO_MODE (0) + // TS_RFX_CONTEXT::properties::cct = COL_CONV_ICT (1) + // TS_RFX_CONTEXT::properties::xft = CLW_XFORM_DWT_53_A (1) + // TS_RFX_CONTEXT::properties::et = CLW_ENTROPY_RLGR3 (4) + // TS_RFX_CONTEXT::properties::qt = SCALAR_QUANTIZATION (1) + // TS_RFX_CONTEXT::properties::r = RESERVED +]; + +const CONTEXT_PDU_BUFFER_WITH_ZERO_DATA_LENGTH: [u8; 13] = [ + 0xc3, 0xcc, // TS_RFX_CONTEXT::CodecChannelT::BlockT::blockType = WBT_CONTEXT + 0x01, 0x00, 0x00, 0x00, // TS_RFX_CONTEXT::CodecChannelT::BlockT::blockLen = 1 + 0x01, // TS_RFX_CONTEXT::CodecChannelT::codecId = 1 + 0xff, // TS_RFX_CONTEXT::CodecChannelT::channelId = 255 + 0x00, // TS_RFX_CONTEXT::ctxId = 0 + 0x40, 0x00, // TS_RFX_CONTEXT::tileSize = 64 + 0x28, 0x28, // TS_RFX_CONTEXT::properties +]; + +const CONTEXT_PDU_BUFFER_WITH_BIG_DATA_LENGTH: [u8; 13] = [ + 0xc3, 0xcc, // TS_RFX_CONTEXT::CodecChannelT::BlockT::blockType = WBT_CONTEXT + 0xff, 0x00, 0x00, 0x00, // TS_RFX_CONTEXT::CodecChannelT::BlockT::blockLen = 0xff + 0x01, // TS_RFX_CONTEXT::CodecChannelT::codecId = 1 + 0xff, // TS_RFX_CONTEXT::CodecChannelT::channelId = 255 + 0x00, // TS_RFX_CONTEXT::ctxId = 0 + 0x40, 0x00, // TS_RFX_CONTEXT::tileSize = 64 + 0x28, 0x28, // TS_RFX_CONTEXT::properties +]; + +const FRAME_BEGIN_PDU_BUFFER: [u8; 14] = [ + 0xc4, 0xcc, // TS_RFX_FRAME_BEGIN::CodecChannelT::blockType = WBT_FRAME_BEGIN + 0x0e, 0x00, 0x00, 0x00, // TS_RFX_FRAME_BEGIN::CodecChannelT::blockLen = 14 + 0x01, // TS_RFX_FRAME_BEGIN::CodecChannelT::codecId = 1 + 0x00, // TS_RFX_FRAME_BEGIN::CodecChannelT::channelId = 0 + 0x00, 0x00, 0x00, 0x00, // TS_RFX_FRAME_BEGIN::frameIdx = 0 + 0x01, 0x00, // TS_RFX_FRAME_BEGIN::numRegions = 1 +]; + +const FRAME_END_PDU_BUFFER: [u8; 8] = [ + 0xc5, 0xcc, // TS_RFX_FRAME_END::CodecChannelT::blockType = WBT_FRAME_END + 0x08, 0x00, 0x00, 0x00, // TS_FRAME_END::CodecChannelT::blockLen = 14 + 0x01, // TS_FRAME_END::CodecChannelT::codecId = 1 + 0x00, // TS_FRAME_END::CodecChannelT::channelId = 0 +]; + +const REGION_PDU_BUFFER: [u8; 31] = [ + 0xc6, 0xcc, // TS_RFX_REGION::CodecChannelT::blockType = WBT_REGION + 0x1f, 0x00, 0x00, 0x00, // TS_RFX_REGION::CodecChannelT::blockLen = 31 + 0x01, // TS_RFX_REGION::CodecChannelT::codecId = 1 + 0x00, // TS_RFX_REGION::CodecChannelT::channelId = 0 + 0x01, // TS_RFX_REGION::regionFlags + //TS_RFX_REGION::regionFlags::lrf = 1 + 0x02, 0x00, // TS_RFX_REGION::numRects = 2 + 0x00, 0x00, // TS_RFX_REGION::TS_RFX_RECT::x = 0 + 0x00, 0x00, // TS_RFX_REGION::TS_RFX_RECT::y = 0 + 0x40, 0x00, // TS_RFX_REGION::TS_RFX_RECT::width = 64 + 0x40, 0x00, // TS_RFX_REGION::TS_RFX_RECT::height = 64 + 0x40, 0x00, // TS_RFX_REGION::TS_RFX_RECT::x = 64 + 0x40, 0x00, // TS_RFX_REGION::TS_RFX_RECT::y = 64 + 0xff, 0x00, // TS_RFX_REGION::TS_RFX_RECT::width = 0xff + 0xff, 0x00, // TS_RFX_REGION::TS_RFX_RECT::height = 0xff + 0xc1, 0xca, // TS_RFX_REGION::regionType = CBT_REGION + 0x01, 0x00, // TS_RFX_REGION::numTilesets = 1 +]; + +const TILESET_PDU_BUFFER: [u8; 82] = [ + 0xc7, 0xcc, // TS_RFX_TILESET::CodecChannelT::blockType = WBT_EXTENSION + 0x52, 0x00, 0x00, 0x00, // TS_RFX_TILESET::CodecChannelT::blockLen = 82 + 0x01, // TS_RFX_TILESET::codecId = 1 + 0x00, // TS_RFX_TILESET::channelId = 0 + 0xc2, 0xca, // TS_RFX_TILESET::subtype = CBT_TILESET + 0x00, 0x00, // TS_RFX_TILESET::idx = 0x00 + 0x51, 0x50, // TS_RFX_TILESET::properties + //TS_RFX_TILESET::properties::lt = TRUE (1) + //TS_RFX_TILESET::properties::flags = VIDEO_MODE (0) + //TS_RFX_TILESET::properties::cct = COL_CONV_ICT (1) + //TS_RFX_TILESET::properties::xft = CLW_XFORM_DWT_53_A (1) + //TS_RFX_TILESET::properties::et = CLW_ENTROPY_RLGR3 (4) + //TS_RFX_TILESET::properties::qt = SCALAR_QUANTIZATION (1) + 0x02, // TS_RFX_TILESET::numQuant = 2 + 0x40, // TS_RFX_TILESET::tileSize = 64 + 0x02, 0x00, // TS_RFX_TILESET::numTiles = 2 + 0x32, 0x00, 0x00, 0x00, // TS_RFX_TILESET::tilesDataSize = 50 + 0x66, 0x66, 0x77, 0x88, 0x98, // TS_RFX_TILESET::quant #1 + 0x66, 0x66, 0x77, 0x88, 0x98, // TS_RFX_TILESET::quant #2 + //TS_RFX_TILESET::quantVals::LL3 = 6 + //TS_RFX_TILESET::quantVals::LH3 = 6 + //TS_RFX_TILESET::quantVals::HL3 = 6 + //TS_RFX_TILESET::quantVals::HH3 = 6 + //TS_RFX_TILESET::quantVals::LH2 = 7 + //TS_RFX_TILESET::quantVals::HL2 = 7 + //TS_RFX_TILESET::quantVals::HH2 = 8 + //TS_RFX_TILESET::quantVals::LH1 = 8 + //TS_RFX_TILESET::quantVals::HL1 = 8 + //TS_RFX_TILESET::quantVals::HH1 = 9 + // TILE #1 + 0xc3, 0xca, // TS_RFX_TILE::BlockT::blockType = CBT_TILE + 0x19, 0x00, 0x00, 0x00, // TS_RFX_TILE::BlockT::blockLen = 25 + 0x00, // TS_RFX_TILE::quantIdxY = 0 + 0x00, // TS_RFX_TILE::quantIdxCb = 0 + 0x00, // TS_RFX_TILE::quantIdxCr = 0 + 0x00, 0x00, // TS_RFX_TILE::xIdx = 0 + 0x00, 0x00, // TS_RFX_TILE::yIdx = 0 + 0x01, 0x00, // TS_RFX_TILE::YLen = 1 + 0x02, 0x00, // TS_RFX_TILE::CbLen = 2 + 0x03, 0x00, // TS_RFX_TILE::CrLen = 3 + 0xf0, // TS_RFX_TILE::YData + 0xf1, 0xf2, // TS_RFX_TILE::CbData + 0xf3, 0xf4, 0xf5, // TS_RFX_TILE::CrData + // TILE #2 + 0xc3, 0xca, // TS_RFX_TILE::BlockT::blockType = CBT_TILE + 0x19, 0x00, 0x00, 0x00, // TS_RFX_TILE::BlockT::blockLen = 25 + 0xff, // TS_RFX_TILE::quantIdxY = 0 + 0xff, // TS_RFX_TILE::quantIdxCb = 0 + 0xff, // TS_RFX_TILE::quantIdxCr = 0 + 0xff, 0xff, // TS_RFX_TILE::xIdx = 0 + 0xff, 0xff, // TS_RFX_TILE::yIdx = 0 + 0x01, 0x00, // TS_RFX_TILE::YLen = 1 + 0x02, 0x00, // TS_RFX_TILE::CbLen = 2 + 0x03, 0x00, // TS_RFX_TILE::CrLen = 3 + 0xf6, // TS_RFX_TILE::YData + 0xf7, 0xf8, // TS_RFX_TILE::CbData + 0xf9, 0xfa, 0xfb, // TS_RFX_TILE::CrData +]; + +const TILE1_Y_DATA: [u8; 1] = [0xf0]; + +const TILE1_CB_DATA: [u8; 2] = [0xf1, 0xf2]; + +const TILE1_CR_DATA: [u8; 3] = [0xf3, 0xf4, 0xf5]; + +const TILE2_Y_DATA: [u8; 1] = [0xf6]; + +const TILE2_CB_DATA: [u8; 2] = [0xf7, 0xf8]; + +const TILE2_CR_DATA: [u8; 3] = [0xf9, 0xfa, 0xfb]; + +const TILESET_PDU_BUFFER_WITH_INVALID_NUMBER_OF_QUANTS: [u8; 27] = [ + 0xc7, 0xcc, // TS_RFX_TILESET::CodecChannelT::blockType = WBT_EXTENSION + 0xd9, 0x03, 0x00, 0x00, // TS_RFX_TILESET::CodecChannelT::blockLen = 985 + 0x01, // TS_RFX_TILESET::codecId = 1 + 0x00, // TS_RFX_TILESET::channelId = 0 + 0xc2, 0xca, // TS_RFX_TILESET::subtype = CBT_TILESET + 0x00, 0x00, // TS_RFX_TILESET::idx = 0x00 + 0x51, 0x50, // TS_RFX_TILESET::properties + 0x0f, // TS_RFX_TILESET::numQuant = 0x0f + 0x40, // TS_RFX_TILESET::tileSize = 64 + 0x01, 0x00, // TS_RFX_TILESET::numTiles = 1 + 0xdf, 0x03, 0x00, 0x00, // TS_RFX_TILESET::tilesDataSize = 991 + 0x66, 0x66, 0x77, 0x88, 0x98, // TS_RFX_TILESET::quantVals +]; + +const TILESET_PDU_BUFFER_WITH_INVALID_TILES_DATA_SIZE: [u8; 27] = [ + 0xc7, 0xcc, // TS_RFX_TILESET::CodecChannelT::blockType = WBT_EXTENSION + 0xd9, 0x03, 0x00, 0x00, // TS_RFX_TILESET::CodecChannelT::blockLen = 985 + 0x01, // TS_RFX_TILESET::codecId = 1 + 0x00, // TS_RFX_TILESET::channelId = 0 + 0xc2, 0xca, // TS_RFX_TILESET::subtype = CBT_TILESET + 0x00, 0x00, // TS_RFX_TILESET::idx = 0x00 + 0x51, 0x50, // TS_RFX_TILESET::properties + 0x0f, // TS_RFX_TILESET::numQuant = 0x0f + 0x40, // TS_RFX_TILESET::tileSize = 64 + 0x01, 0x00, // TS_RFX_TILESET::numTiles = 1 + 0xff, 0xff, 0xff, 0xff, // TS_RFX_TILESET::tilesDataSize = 0xffff_ffff + 0x66, 0x66, 0x77, 0x88, 0x98, // TS_RFX_TILESET::quantVals +]; + +const SYNC_PDU: Block<'_> = Block::Sync(SyncPdu); + +const CODEC_VERSIONS_PDU: Block<'_> = Block::CodecVersions(CodecVersionsPdu); + +const CONTEXT_PDU: Block<'_> = Block::CodecChannel(CodecChannel::Context(ContextPdu { + flags: OperatingMode::empty(), + entropy_algorithm: EntropyAlgorithm::Rlgr3, +})); + +const FRAME_BEGIN_PDU: Block<'_> = Block::CodecChannel(CodecChannel::FrameBegin(FrameBeginPdu { + index: 0, + number_of_regions: 1, +})); + +const FRAME_END_PDU: Block<'_> = Block::CodecChannel(CodecChannel::FrameEnd(FrameEndPdu)); + +static CHANNELS_PDU: LazyLock> = LazyLock::new(|| { + Block::Channels(ChannelsPdu(vec![ + RfxChannel { width: 64, height: 64 }, + RfxChannel { width: 32, height: 32 }, + ])) +}); +static REGION_PDU: LazyLock> = LazyLock::new(|| { + Block::CodecChannel(CodecChannel::Region(RegionPdu { + rectangles: vec![ + RfxRectangle { + x: 0, + y: 0, + width: 64, + height: 64, + }, + RfxRectangle { + x: 64, + y: 64, + width: 0xff, + height: 0xff, + }, + ], + })) +}); +static TILESET_PDU: LazyLock> = LazyLock::new(|| { + Block::CodecChannel(CodecChannel::TileSet(TileSetPdu { + entropy_algorithm: EntropyAlgorithm::Rlgr3, + quants: vec![ + Quant { + ll3: 6, + lh3: 6, + hl3: 6, + hh3: 6, + lh2: 7, + hl2: 7, + hh2: 8, + lh1: 8, + hl1: 8, + hh1: 9, + }; + 2 + ], + tiles: vec![ + Tile { + y_quant_index: 0, + cb_quant_index: 0, + cr_quant_index: 0, + + x: 0, + y: 0, + + y_data: &TILE1_Y_DATA, + cb_data: &TILE1_CB_DATA, + cr_data: &TILE1_CR_DATA, + }, + Tile { + y_quant_index: 0xff, + cb_quant_index: 0xff, + cr_quant_index: 0xff, + + x: 0xffff, + y: 0xffff, + + y_data: &TILE2_Y_DATA, + cb_data: &TILE2_CB_DATA, + cr_data: &TILE2_CR_DATA, + }, + ], + })) +}); + +#[test] +fn from_buffer_for_block_header_returns_error_on_zero_data_length() { + decode::>(SYNC_PDU_BUFFER_WITH_ZERO_DATA_LENGTH.as_ref()).unwrap_err(); +} + +#[test] +fn from_buffer_for_block_header_returns_error_on_data_length_greater_then_available_data() { + decode::>(SYNC_PDU_BUFFER_WITH_BIG_DATA_LENGTH.as_ref()).unwrap_err(); +} + +#[test] +fn from_buffer_for_pdu_with_codec_channel_header_returns_error_on_small_buffer() { + decode::>(SYNC_PDU_BUFFER_WITH_SMALL_BUFFER.as_ref()).unwrap_err(); +} + +#[test] +fn from_buffer_returns_error_on_invalid_data_length_for_channels_pdu() { + decode::>(CHANNELS_PDU_BUFFER_WITH_INVALID_DATA_LENGTH.as_ref()).unwrap_err(); +} + +encode_decode_test! { + sync: SYNC_PDU, SYNC_PDU_BUFFER; + codec_version: CODEC_VERSIONS_PDU, CODEC_VERSIONS_PDU_BUFFER; + channels: CHANNELS_PDU.clone(), CHANNELS_PDU_BUFFER; + context: CONTEXT_PDU.clone(), CONTEXT_PDU_BUFFER; + region: REGION_PDU.clone(), REGION_PDU_BUFFER; + frame_begin: FRAME_BEGIN_PDU, FRAME_BEGIN_PDU_BUFFER; + frame_end: FRAME_END_PDU, FRAME_END_PDU_BUFFER; + tile_set: TILESET_PDU.clone(), TILESET_PDU_BUFFER; +} + +#[test] +fn from_buffer_for_codec_channel_header_returns_error_on_zero_data_length() { + decode::>(CONTEXT_PDU_BUFFER_WITH_ZERO_DATA_LENGTH.as_ref()).unwrap_err(); +} + +#[test] +fn from_buffer_for_codec_channel_header_returns_error_on_data_length_greater_then_available_data() { + decode::>(CONTEXT_PDU_BUFFER_WITH_BIG_DATA_LENGTH.as_ref()).unwrap_err(); +} + +#[test] +fn from_buffer_returns_error_on_invalid_number_of_quants_for_tile_set_pdu() { + decode::>(TILESET_PDU_BUFFER_WITH_INVALID_NUMBER_OF_QUANTS.as_ref()).unwrap_err(); +} + +#[test] +fn from_buffer_returns_error_on_invalid_tiles_data_size_for_tile_set_pdu() { + decode::>(TILESET_PDU_BUFFER_WITH_INVALID_TILES_DATA_SIZE.as_ref()).unwrap_err(); +} diff --git a/crates/ironrdp-testsuite-core/tests/pdu/x224.rs b/crates/ironrdp-testsuite-core/tests/pdu/x224.rs new file mode 100644 index 00000000..fcbde2c4 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/x224.rs @@ -0,0 +1,349 @@ +use expect_test::expect; +use ironrdp_core::{ReadCursor, WriteCursor}; +use ironrdp_pdu::nego::{ + ConnectionConfirm, ConnectionRequest, Cookie, FailureCode, NegoRequestData, RequestFlags, ResponseFlags, + RoutingToken, SecurityProtocol, +}; +use ironrdp_pdu::tpdu::{TpduCode, TpduHeader}; +use ironrdp_pdu::tpkt::TpktHeader; +use ironrdp_pdu::x224::{user_data_size, X224}; +use ironrdp_testsuite_core::encode_decode_test; + +const SAMPLE_TPKT_HEADER_BINARY: [u8; 4] = [ + 0x3, // version + 0x0, // reserved + 0x5, 0x42, // length in BE +]; + +const SAMPLE_TPKT_HEADER: TpktHeader = TpktHeader { packet_length: 0x542 }; + +#[test] +fn tpkt_header_write() { + let mut buffer = [0; 4]; + let mut cursor = WriteCursor::new(&mut buffer); + SAMPLE_TPKT_HEADER.write(&mut cursor).unwrap(); + assert_eq!(cursor.inner(), SAMPLE_TPKT_HEADER_BINARY); +} + +#[test] +fn tpkt_header_read() { + let mut cursor = ReadCursor::new(&SAMPLE_TPKT_HEADER_BINARY); + let tpkt = TpktHeader::read(&mut cursor).unwrap(); + assert_eq!(tpkt, SAMPLE_TPKT_HEADER); +} + +#[test] +fn tpdu_header_read() { + let mut src = ReadCursor::new(&[ + 0x03, 0x00, 0x00, 0x0c, // tpkt + 0x02, 0xf0, 0x80, // tpdu + 0x04, 0x01, 0x00, 0x01, 0x00, // payload + ]); + + let tpkt = TpktHeader::read(&mut src).expect("tpkt"); + assert_eq!(tpkt.packet_length, 12); + + let tpdu = TpduHeader::read(&mut src, &tpkt).expect("tpdu"); + assert_eq!(tpdu.li, 2); + assert_eq!(tpdu.code, TpduCode::DATA); + assert_eq!(tpdu.fixed_part_size(), 3); + assert_eq!(tpdu.variable_part_size(), 0); + + let payload_len = user_data_size(&tpkt, &tpdu); + assert_eq!(payload_len, 5); + assert_eq!(src.len(), payload_len); +} + +#[test] +fn tpdu_header_write() { + let expected = [ + 0x02, 0xf0, 0x80, // data tpdu + ]; + + let mut buffer = [0; 3]; + let mut cursor = WriteCursor::new(&mut buffer); + + TpduHeader { + li: 2, + code: TpduCode::DATA, + } + .write(&mut cursor) + .unwrap(); + + assert_eq!(buffer, expected); +} + +encode_decode_test! { + nego_connection_request_rdp_security_without_cookie: + X224(ConnectionRequest { + nego_data: None, + flags: RequestFlags::empty(), + protocol: SecurityProtocol::empty(), + }), + [ + // tpkt header + 0x03, // version + 0x00, // reserved + 0x00, 0x13, // length in BE + // tpdu header + 0x0E, // length + 0xE0, // code + 0x00, 0x00, // dst_ref + 0x00, 0x00, // src_ref + 0x00, // class + // variable part + 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // RDP_NEG_REQ + ]; + + nego_connection_request_rdp_security_with_cookie: + X224(ConnectionRequest { + nego_data: Some(NegoRequestData::Cookie(Cookie("User".to_owned()))), + flags: RequestFlags::empty(), + protocol: SecurityProtocol::empty(), + }), + [ + // tpkt header + 0x03, // version + 0x00, // reserved + 0x00, 0x2A, // length in BE + // tpdu header + 0x25, // length + 0xE0, // code + 0x00, 0x00, // dst_ref + 0x00, 0x00, // src_ref + 0x00, // class + // variable part + 0x43, 0x6F, 0x6F, 0x6B, 0x69, 0x65, 0x3A, 0x20, 0x6D, 0x73, 0x74, 0x73, 0x68, 0x61, 0x73, 0x68, 0x3D, 0x55, + 0x73, 0x65, 0x72, 0x0D, 0x0A, // cookie + 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // RDP_NEG_REQ + ]; + + nego_connection_request_ssl_security_with_cookie: + X224(ConnectionRequest { + nego_data: Some(NegoRequestData::Cookie(Cookie("User".to_owned()))), + flags: RequestFlags::empty(), + protocol: SecurityProtocol::HYBRID | SecurityProtocol::SSL, + }), + [ + // tpkt header + 0x03, // version + 0x00, // reserved + 0x00, 0x2A, // length in BE + // tpdu header + 0x25, // length + 0xE0, // code + 0x00, 0x00, // dst_ref + 0x00, 0x00, // src_ref + 0x00, // class + // variable part + 0x43, 0x6F, 0x6F, 0x6B, 0x69, 0x65, 0x3A, 0x20, 0x6D, 0x73, 0x74, 0x73, 0x68, 0x61, 0x73, 0x68, 0x3D, 0x55, + 0x73, 0x65, 0x72, 0x0D, 0x0A, // cookie + 0x01, 0x00, 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, // RDP_NEG_REQ + ]; + + nego_connection_request_ssl_security_with_flags: + X224(ConnectionRequest { + nego_data: Some(NegoRequestData::Cookie(Cookie("User".to_owned()))), + flags: RequestFlags::RESTRICTED_ADMIN_MODE_REQUIRED | RequestFlags::REDIRECTED_AUTHENTICATION_MODE_REQUIRED, + protocol: SecurityProtocol::HYBRID | SecurityProtocol::SSL, + }), + [ + // tpkt header + 0x03, // version + 0x00, // reserved + 0x00, 0x2A, // length in BE + // tpdu header + 0x25, // length + 0xE0, // code + 0x00, 0x00, // dst_ref + 0x00, 0x00, // src_ref + 0x00, // class + // cookie + 0x43, 0x6F, 0x6F, 0x6B, 0x69, 0x65, 0x3A, 0x20, 0x6D, 0x73, 0x74, 0x73, 0x68, 0x61, 0x73, 0x68, 0x3D, 0x55, + 0x73, 0x65, 0x72, 0x0D, 0x0A, + // RDP_NEG_REQ + 0x01, // type + 0x03, // flags + 0x08, 0x00, // length + 0x03, 0x00, 0x00, 0x00, // request message + ]; + + nego_confirm_response: + X224(ConnectionConfirm::Response { + flags: ResponseFlags::from_bits_truncate(0x1F), + protocol: SecurityProtocol::HYBRID, + }), + [ + // tpkt header + 0x03, // version + 0x00, // reserved + 0x00, 0x13, // length in BE + // tpdu header + 0x0E, // length + 0xD0, // code + 0x00, 0x00, // dst_ref + 0x00, 0x00, // src_ref + 0x00, // class + // RDP_NEG_RSP + 0x02, // type + 0x1F, // flags + 0x08, 0x00, // length + 0x02, 0x00, 0x00, 0x00, // selected protocol + ]; + + nego_confirm_failure: + X224(ConnectionConfirm::Failure { + code: FailureCode::SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER, + }), + [ + // tpkt header + 0x03, // version + 0x00, // reserved + 0x00, 0x13, // length in BE + // tpdu header + 0x0E, // length + 0xD0, // code + 0x00, 0x00, // dst_ref + 0x00, 0x00, // src_ref + 0x00, // class + // RDP_NEG_FAILURE + 0x03, // type + 0x00, // flags + 0x08, 0x00, // length + 0x06, 0x00, 0x00, 0x00, // failure code + ]; +} + +#[test] +fn nego_request_unexpected_rdp_msg_type() { + let payload = [ + // tpkt header + 0x03, // version + 0x00, // reserved + 0x00, 0x2A, // length in BE + // tpdu header + 0x25, // length + 0xE0, // code + 0x00, 0x00, // dst_ref + 0x00, 0x00, // src_ref + 0x00, // class + // variable part + 0x43, 0x6F, 0x6F, 0x6B, 0x69, 0x65, 0x3A, 0x20, 0x6D, 0x73, 0x74, 0x73, 0x68, 0x61, 0x73, 0x68, 0x3D, 0x55, + 0x73, 0x65, 0x72, 0x0D, 0x0A, // cookie + // RDP message + 0x03, // type + 0x00, // flags + 0x08, 0x00, // length + 0x03, 0x00, 0x00, 0x00, // rest + ]; + + let e = ironrdp_core::decode::>(&payload).err().unwrap(); + + expect![[r#" + Error { + context: "Client X.224 Connection Request", + kind: UnexpectedMessageType { + got: 3, + }, + source: None, + } + "#]] + .assert_debug_eq(&e); +} + +#[test] +fn nego_confirm_unexpected_rdp_msg_type() { + let payload = [ + // tpkt header + 0x03, // version + 0x00, // reserved + 0x00, 0x13, // length in BE + // tpdu header + 0x0E, // length + 0xD0, // code + 0x00, 0x00, // dst_ref + 0x00, 0x00, // src_ref + 0x00, // class + // RDP_NEG_REQ + 0xAF, // type + 0x1F, // flags + 0x08, 0x00, // length + 0x02, 0x00, 0x00, 0x00, // selected protocol + ]; + + let e = ironrdp_core::decode::>(&payload).err().unwrap(); + + expect![[r#" + Error { + context: "Server X.224 Connection Confirm", + kind: UnexpectedMessageType { + got: 175, + }, + source: None, + } + "#]] + .assert_debug_eq(&e); +} + +#[test] +fn cookie_decode() { + let payload = [ + 0x43, 0x6F, 0x6F, 0x6B, 0x69, 0x65, 0x3A, 0x20, 0x6D, 0x73, 0x74, 0x73, 0x68, 0x61, 0x73, 0x68, 0x3D, 0x55, + 0x73, 0x65, 0x72, 0x0D, 0x0A, 0xFF, 0xFF, + ]; + + let cookie = Cookie::read(&mut ReadCursor::new(&payload)) + .expect("read cookie") + .expect("cookie"); + + assert_eq!(cookie.0, "User"); +} + +#[test] +fn routing_token_decode() { + let payload = [ + 0x43, 0x6F, 0x6F, 0x6B, 0x69, 0x65, 0x3A, 0x20, 0x6D, 0x73, 0x74, 0x73, 0x3D, 0x33, 0x36, 0x34, 0x30, 0x32, + 0x30, 0x35, 0x32, 0x32, 0x38, 0x2E, 0x31, 0x35, 0x36, 0x32, 0x39, 0x2E, 0x30, 0x30, 0x30, 0x30, 0x0D, 0x0A, + 0xFF, 0xFF, + ]; + + let routing_token = RoutingToken::read(&mut ReadCursor::new(&payload)) + .expect("read routing token") + .expect("routing token"); + + assert_eq!(routing_token.0, "3640205228.15629.0000"); +} + +#[test] +fn not_a_cookie_decode() { + let payload = [ + 0x6e, 0x6f, 0x74, 0x20, 0x61, 0x20, 0x63, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x0F, 0x42, 0x73, 0x65, 0x72, 0x0D, + 0x0A, 0xFF, 0xFF, + ]; + + let maybe_cookie = Cookie::read(&mut ReadCursor::new(&payload)).expect("read cookie"); + + assert!(maybe_cookie.is_none()); +} + +#[test] +fn cookie_without_cr_lf_error_decode() { + let payload = [ + 0x43, 0x6F, 0x6F, 0x6B, 0x69, 0x65, 0x3A, 0x20, 0x6D, 0x73, 0x74, 0x73, 0x68, 0x61, 0x73, 0x68, 0x3D, 0x55, + 0x73, 0x65, 0x72, + ]; + + let e = Cookie::read(&mut ReadCursor::new(&payload)).err().unwrap(); + + expect![[r#" + Error { + context: "Cookie", + kind: NotEnoughBytes { + received: 1, + expected: 2, + }, + source: None, + } + "#]] + .assert_debug_eq(&e); +} diff --git a/crates/ironrdp-testsuite-core/tests/propertyset.rs b/crates/ironrdp-testsuite-core/tests/propertyset.rs new file mode 100644 index 00000000..bbd4b798 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/propertyset.rs @@ -0,0 +1,120 @@ +use expect_test::expect; +use ironrdp_rdpfile::ParseResult; + +const RDP_FILE_SAMPLE: &str = "remoteapplicationmode:i:0 +server port:i:3389 +promptcredentialonce:i:1 +full address:s:192.168.56.101 +alternate shell:s:|explorer +remoteapplicationname:s:|explorer +alternate full address:s:some.alternateaddress.ninja +username:s:David +ClearTextPassword:s:Devolutions123! +MalformedLine:s +UnknownType:z:10293"; + +const RDP_FILE_SAMPLE_2: &str = "remoteapplicationmode:i:50 +server port:i:4000 +full address:s:192.168.56.2"; + +#[test] +fn parse_file() { + let ParseResult { mut properties, errors } = ironrdp_rdpfile::parse(RDP_FILE_SAMPLE); + + expect![[r#" + { + "ClearTextPassword": Str( + "Devolutions123!", + ), + "alternate full address": Str( + "some.alternateaddress.ninja", + ), + "alternate shell": Str( + "|explorer", + ), + "full address": Str( + "192.168.56.101", + ), + "promptcredentialonce": Int( + 1, + ), + "remoteapplicationmode": Int( + 0, + ), + "remoteapplicationname": Str( + "|explorer", + ), + "server port": Int( + 3389, + ), + "username": Str( + "David", + ), + } + "#]] + .assert_debug_eq(&properties); + + expect![[r#" + [ + Error { + kind: MalformedLine { + line: "MalformedLine:s", + }, + line: 9, + }, + Error { + kind: UnknownType { + ty: "z", + }, + line: 10, + }, + ] + "#]] + .assert_debug_eq(&errors); + + // Verify the `get` operation. + assert_eq!(properties.get::("remoteapplicationmode"), Some(false)); + assert_eq!(properties.get::("promptcredentialonce"), Some(true)); + assert_eq!(properties.get::("absentproperty"), None); + assert_eq!(properties.get::("server port"), Some(3389)); + assert_eq!(properties.get::<&str>("full address"), Some("192.168.56.101")); + + // Merge another file. + ironrdp_rdpfile::load(&mut properties, RDP_FILE_SAMPLE_2).expect("valid rdp file format"); + + expect![[r#" + { + "ClearTextPassword": Str( + "Devolutions123!", + ), + "alternate full address": Str( + "some.alternateaddress.ninja", + ), + "alternate shell": Str( + "|explorer", + ), + "full address": Str( + "192.168.56.2", + ), + "promptcredentialonce": Int( + 1, + ), + "remoteapplicationmode": Int( + 50, + ), + "remoteapplicationname": Str( + "|explorer", + ), + "server port": Int( + 4000, + ), + "username": Str( + "David", + ), + } + "#]] + .assert_debug_eq(&properties); + + // Anything that is not 0 is considered to be 'true'. + assert_eq!(properties.get::("remoteapplicationmode"), Some(true)); +} diff --git a/crates/ironrdp-testsuite-core/tests/rdcleanpath.rs b/crates/ironrdp-testsuite-core/tests/rdcleanpath.rs new file mode 100644 index 00000000..9c4cf28d --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/rdcleanpath.rs @@ -0,0 +1,187 @@ +use expect_test::{expect, Expect}; +use ironrdp_rdcleanpath::{ + DetectionResult, RDCleanPathErr, RDCleanPathPdu, GENERAL_ERROR_CODE, NEGOTIATION_ERROR_CODE, VERSION_1, +}; +use rstest::rstest; + +fn request() -> RDCleanPathPdu { + RDCleanPathPdu::new_request( + vec![0xDE, 0xAD, 0xBE, 0xFF], + "destination".to_owned(), + "proxy auth".to_owned(), + Some("PCB".to_owned()), + ) + .unwrap() +} + +const REQUEST_DER: &[u8] = &[ + 0x30, 0x32, 0xA0, 0x4, 0x2, 0x2, 0xD, 0x3E, 0xA2, 0xD, 0xC, 0xB, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6E, 0x61, 0x74, + 0x69, 0x6F, 0x6E, 0xA3, 0xC, 0xC, 0xA, 0x70, 0x72, 0x6F, 0x78, 0x79, 0x20, 0x61, 0x75, 0x74, 0x68, 0xA5, 0x5, 0xC, + 0x3, 0x50, 0x43, 0x42, 0xA6, 0x6, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, +]; + +fn response_success() -> RDCleanPathPdu { + RDCleanPathPdu::new_response( + "192.168.7.95".to_owned(), + vec![0xDE, 0xAD, 0xBE, 0xFF], + [ + vec![0xDE, 0xAD, 0xBE, 0xFF], + vec![0xDE, 0xAD, 0xBE, 0xFF], + vec![0xDE, 0xAD, 0xBE, 0xFF], + ], + ) + .unwrap() +} + +const RESPONSE_SUCCESS_DER: &[u8] = &[ + 0x30, 0x34, 0xA0, 0x4, 0x2, 0x2, 0xD, 0x3E, 0xA6, 0x6, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0xA7, 0x14, 0x30, 0x12, + 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0xA9, 0xE, + 0xC, 0xC, 0x31, 0x39, 0x32, 0x2E, 0x31, 0x36, 0x38, 0x2E, 0x37, 0x2E, 0x39, 0x35, +]; + +fn response_http_error() -> RDCleanPathPdu { + RDCleanPathPdu::new_http_error(500) +} + +const RESPONSE_HTTP_ERROR_DER: &[u8] = &[ + 0x30, 0x15, 0xA0, 0x4, 0x2, 0x2, 0xD, 0x3E, 0xA1, 0xD, 0x30, 0xB, 0xA0, 0x3, 0x2, 0x1, 0x1, 0xA1, 0x4, 0x2, 0x2, + 0x1, 0xF4, +]; + +fn response_tls_error() -> RDCleanPathPdu { + RDCleanPathPdu::new_tls_error(48) +} + +const RESPONSE_TLS_ERROR_DER: &[u8] = &[ + 0x30, 0x14, 0xA0, 0x04, 0x02, 0x02, 0x0D, 0x3E, 0xA1, 0x0C, 0x30, 0x0A, 0xA0, 0x03, 0x02, 0x01, 0x01, 0xA3, 0x03, + 0x02, 0x01, 0x30, +]; + +#[rstest] +#[case(request())] +#[case(response_success())] +#[case(response_http_error())] +#[case(response_tls_error())] +fn smoke(#[case] message: RDCleanPathPdu) { + let encoded = message.to_der().unwrap(); + let decoded = RDCleanPathPdu::from_der(&encoded).unwrap(); + assert_eq!(message, decoded); +} + +macro_rules! assert_serialization { + ($left:expr, $right:expr) => {{ + if $left != $right { + let left = hex::encode(&$left); + let right = hex::encode(&$right); + let comparison = pretty_assertions::StrComparison::new(&left, &right); + panic!( + "assertion failed: `({} == {})`\n\n{comparison}", + stringify!($left), + stringify!($right), + ); + } + }}; +} + +#[rstest] +#[case(request(), REQUEST_DER)] +#[case(response_success(), RESPONSE_SUCCESS_DER)] +#[case(response_http_error(), RESPONSE_HTTP_ERROR_DER)] +#[case(response_tls_error(), RESPONSE_TLS_ERROR_DER)] +fn serialization(#[case] message: RDCleanPathPdu, #[case] expected_der: &[u8]) { + let encoded = message.to_der().unwrap(); + assert_serialization!(encoded, expected_der); +} + +#[rstest] +#[case(REQUEST_DER)] +#[case(RESPONSE_SUCCESS_DER)] +#[case(RESPONSE_HTTP_ERROR_DER)] +#[case(RESPONSE_TLS_ERROR_DER)] +fn detect(#[case] der: &[u8]) { + let result = RDCleanPathPdu::detect(der); + + let DetectionResult::Detected { + version: detected_version, + total_length: detected_length, + } = result + else { + panic!("unexpected result: {result:?}"); + }; + + assert_eq!(detected_version, VERSION_1); + assert_eq!(detected_length, der.len()); +} + +#[rstest] +#[case(&[])] +#[case(&[0x30])] +#[case(&[0x30, 0x15])] +#[case(&[0x30, 0x15, 0xA0])] +#[case(&[0x30, 0x32, 0xA0, 0x4])] +#[case(&[0x30, 0x32, 0xA0, 0x4, 0x2])] +#[case(&[0x30, 0x32, 0xA0, 0x4, 0x2, 0x2])] +#[case(&[0x30, 0x32, 0xA0, 0x4, 0x2, 0x2, 0xD])] +fn detect_not_enough(#[case] payload: &[u8]) { + let result = RDCleanPathPdu::detect(payload); + assert_eq!(result, DetectionResult::NotEnoughBytes); +} + +#[rstest] +#[case::http( + RDCleanPathErr { + error_code: GENERAL_ERROR_CODE, + http_status_code: Some(404), + wsa_last_error: None, + tls_alert_code: None, + }, + expect!["general error (code 1); HTTP 404 not found"], +)] +#[case::wsa( + RDCleanPathErr { + error_code: GENERAL_ERROR_CODE, + http_status_code: None, + wsa_last_error: Some(10061), + tls_alert_code: None, + }, + expect!["general error (code 1); WSA 10061 connection refused"], +)] +#[case::tls( + RDCleanPathErr { + error_code: GENERAL_ERROR_CODE, + http_status_code: None, + wsa_last_error: None, + tls_alert_code: Some(40), + }, + expect!["general error (code 1); TLS alert 40 handshake failure"], +)] +#[case::nego( + RDCleanPathErr { + error_code: NEGOTIATION_ERROR_CODE, + http_status_code: None, + wsa_last_error: None, + tls_alert_code: None, + }, + expect!["negotiation error (code 2)"], +)] +#[case::combined( + RDCleanPathErr { + error_code: GENERAL_ERROR_CODE, + http_status_code: Some(502), + wsa_last_error: Some(10060), + tls_alert_code: Some(45), + }, + expect!["general error (code 1); HTTP 502 bad gateway; WSA 10060 connection timed out; TLS alert 45 certificate expired"], +)] +#[case::unknown_codes( + RDCleanPathErr { + error_code: 99, + http_status_code: Some(999), + wsa_last_error: Some(65000), + tls_alert_code: Some(255), + }, + expect!["unknown error (code 99); HTTP 999 unknown HTTP status; WSA 65000 unknown WSA error; TLS alert 255 unknown TLS alert"], +)] +fn error_display(#[case] error: RDCleanPathErr, #[case] expected: Expect) { + expected.assert_eq(&error.to_string()); +} diff --git a/crates/ironrdp-testsuite-core/tests/rdpsnd/mod.rs b/crates/ironrdp-testsuite-core/tests/rdpsnd/mod.rs new file mode 100644 index 00000000..a175e100 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/rdpsnd/mod.rs @@ -0,0 +1,189 @@ +use std::borrow::Cow; + +use ironrdp_rdpsnd::pdu; +use ironrdp_testsuite_core::encode_decode_test; + +encode_decode_test! { + server_format: pdu::ServerAudioOutputPdu::AudioFormat(pdu::ServerAudioFormatPdu { + version: pdu::Version::V5, + formats: vec![ + pdu::AudioFormat { + format: pdu::WaveFormat::PCM, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 88200, + n_block_align: 4, + bits_per_sample: 16, + data: None, + }, + pdu::AudioFormat { + format: pdu::WaveFormat::ALAW, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 44100, + n_block_align: 2, + bits_per_sample: 8, + data: None, + }, + pdu::AudioFormat { + format: pdu::WaveFormat::MULAW, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 44100, + n_block_align: 2, + bits_per_sample: 8, + data: None, + }, + pdu::AudioFormat { + format: pdu::WaveFormat::ADPCM, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 22311, + n_block_align: 1024, + bits_per_sample: 4, + data: Some(vec![ + 0xf4, 0x03, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x40, 0x00, 0xf0, 0x00, 0x00, + 0x00, 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff, + ]) + }, + pdu::AudioFormat { + format: pdu::WaveFormat::DVI_ADPCM, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 22201, + n_block_align: 1024, + bits_per_sample: 4, + data: Some(vec![ + 0xf9, 0x03, + ]) + }, + ], + }), + [ + 0x07, 0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x22, 0x56, 0x00, 0x00, + 0x88, 0x58, 0x01, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x22, 0x56, + 0x00, 0x00, 0x44, 0xac, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, 0x00, 0x00, 0x07, 0x00, 0x02, 0x00, + 0x22, 0x56, 0x00, 0x00, 0x44, 0xac, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x00, + 0x02, 0x00, 0x22, 0x56, 0x00, 0x00, 0x27, 0x57, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x20, 0x00, + 0xf4, 0x03, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x00, 0x40, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff, + 0x11, 0x00, 0x02, 0x00, 0x22, 0x56, 0x00, 0x00, 0xb9, 0x56, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, + 0x02, 0x00, 0xf9, 0x03, + ]; + client_format: pdu::ClientAudioOutputPdu::AudioFormat(pdu::ClientAudioFormatPdu { + version: pdu::Version::V5, + flags: pdu::AudioFormatFlags::ALIVE | pdu::AudioFormatFlags::VOLUME, + volume_left: 0xFFFF, + volume_right: 0xFFFF, + pitch: 0xF9F700, + dgram_port: 0, + formats: vec![ + pdu::AudioFormat { + format: pdu::WaveFormat::PCM, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 88200, + n_block_align: 4, + bits_per_sample: 16, + data: None, + }, + pdu::AudioFormat { + format: pdu::WaveFormat::ALAW, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 44100, + n_block_align: 2, + bits_per_sample: 8, + data: None, + }, + pdu::AudioFormat { + format: pdu::WaveFormat::MULAW, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 44100, + n_block_align: 2, + bits_per_sample: 8, + data: None, + }, + pdu::AudioFormat { + format: pdu::WaveFormat::ADPCM, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 22311, + n_block_align: 1024, + bits_per_sample: 4, + data: Some(vec![ + 0xf4, 0x03, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x40, 0x00, 0xf0, 0x00, 0x00, + 0x00, 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff, + ]) + }, + pdu::AudioFormat { + format: pdu::WaveFormat::DVI_ADPCM, + n_channels: 2, + n_samples_per_sec: 22050, + n_avg_bytes_per_sec: 22201, + n_block_align: 1024, + bits_per_sample: 4, + data: Some(vec![ + 0xf9, 0x03, + ]) + }, + ], + }), + [ + 0x07, 0x00, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf7, 0xf9, 0x00, + 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x22, 0x56, 0x00, 0x00, + 0x88, 0x58, 0x01, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x22, 0x56, + 0x00, 0x00, 0x44, 0xac, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, 0x00, 0x00, 0x07, 0x00, 0x02, 0x00, + 0x22, 0x56, 0x00, 0x00, 0x44, 0xac, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x00, + 0x02, 0x00, 0x22, 0x56, 0x00, 0x00, 0x27, 0x57, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x20, 0x00, + 0xf4, 0x03, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x00, 0x40, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff, + 0x11, 0x00, 0x02, 0x00, 0x22, 0x56, 0x00, 0x00, 0xb9, 0x56, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, + 0x02, 0x00, 0xf9, 0x03, + ]; + training: pdu::ServerAudioOutputPdu::Training(pdu::TrainingPdu { + timestamp: 0x89da, + data: vec![0x42], + }), + [ + 0x06, 0x00, 0x05, 0x00, 0xda, 0x89, 0x09, 0x00, 0x42 + ]; + training_confirm: pdu::ClientAudioOutputPdu::TrainingConfirm(pdu::TrainingConfirmPdu { + timestamp: 0x89da, + pack_size: 0x400, + }), + [ + 0x06, 0x00, 0x04, 0x00, 0xda, 0x89, 0x00, 0x04, + ]; + wave: pdu::ServerAudioOutputPdu::Wave(pdu::WavePdu { + timestamp: 0xadd7, + format_no: 0xf, + block_no: 8, + data: Cow::Borrowed(&[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), + }), + [ + // WaveInfo + 0x02, 0x00, 0x10, 0x00, 0xd7, 0xad, 0x0f, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1, 0x2, 0x3, 0x4, + // Wave + 0x0, 0x0, 0x0, 0x0, 0x5, 0x6, 0x7, 0x8, + ]; + wave_confirm: pdu::ClientAudioOutputPdu::WaveConfirm(pdu::WaveConfirmPdu { + timestamp: 0x5ab7, + block_no: 8 + }), + [ + 0x05, 0x00, 0x04, 0x00, 0xb7, 0x5a, 0x08, 0x00, + ]; + wave2: pdu::ServerAudioOutputPdu::Wave2(pdu::Wave2Pdu { + timestamp: 0xa116, + audio_timestamp: 0xdacb8c2, + format_no: 0x3, + block_no: 2, + data: Cow::Borrowed(&[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), + }), + [ + 0x0D, 0x00, 0x14, 0x00, 0x16, 0xA1, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0xC2, 0xB8, 0xAC, 0x0D, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + ]; +} diff --git a/crates/ironrdp-testsuite-core/tests/server/fast_path.rs b/crates/ironrdp-testsuite-core/tests/server/fast_path.rs new file mode 120000 index 00000000..f7df2434 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/server/fast_path.rs @@ -0,0 +1 @@ +../../../ironrdp-server/src/encoder/fast_path.rs \ No newline at end of file diff --git a/crates/ironrdp-testsuite-core/tests/server/mod.rs b/crates/ironrdp-testsuite-core/tests/server/mod.rs new file mode 100644 index 00000000..6e563040 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/server/mod.rs @@ -0,0 +1 @@ +mod fast_path; diff --git a/crates/ironrdp-testsuite-core/tests/server_name.rs b/crates/ironrdp-testsuite-core/tests/server_name.rs new file mode 100644 index 00000000..addad2ab --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/server_name.rs @@ -0,0 +1,24 @@ +use ironrdp_connector::ServerName; +use rstest::rstest; + +#[rstest] +#[case("somehostname:2345", "somehostname")] +#[case("192.168.56.101:2345", "192.168.56.101")] +#[case("[2001:db8::8a2e:370:7334]:7171", "2001:db8::8a2e:370:7334")] +#[case("[2001:0db8:0000:0000:0000:8a2e:0370:7334]:433", "2001:db8::8a2e:370:7334")] +#[case("[::1]:2222", "::1")] +fn input_with_port_is_sanitized(#[case] input: &str, #[case] expected: &str) { + let result = ServerName::new(input).into_inner(); + assert_eq!(result, expected); +} + +#[rstest] +#[case("somehostname")] +#[case("192.168.56.101")] +#[case("2001:db8::8a2e:370:7334")] +#[case("2001:0db8:0000:0000:0000:8a2e:0370:7334")] +#[case("::1")] +fn input_without_port_is_left_untouched(#[case] input: &str) { + let result = ServerName::new(input).into_inner(); + assert_eq!(result, input); +} diff --git a/crates/ironrdp-testsuite-core/tests/session/mod.rs b/crates/ironrdp-testsuite-core/tests/session/mod.rs new file mode 100644 index 00000000..dda2a99f --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/session/mod.rs @@ -0,0 +1,36 @@ +mod rfx; + +#[cfg(test)] +mod tests { + use ironrdp_pdu::rdp::capability_sets::{client_codecs_capabilities, CodecProperty}; + + #[test] + fn test_codecs_capabilities() { + let config = &[]; + let _capabilities = client_codecs_capabilities(config).unwrap(); + + let config = &["badcodec"]; + assert!(client_codecs_capabilities(config).is_err()); + + let config = &["remotefx:on"]; + let capabilities = client_codecs_capabilities(config).unwrap(); + assert!(capabilities + .0 + .iter() + .any(|cap| matches!(cap.property, CodecProperty::RemoteFx(_)))); + + let config = &["remotefx:off"]; + let capabilities = client_codecs_capabilities(config).unwrap(); + assert!(!capabilities + .0 + .iter() + .any(|cap| matches!(cap.property, CodecProperty::RemoteFx(_)))); + + let config = &["qoi:on"]; + let capabilities = client_codecs_capabilities(config).unwrap(); + assert!(capabilities + .0 + .iter() + .any(|cap| matches!(cap.property, CodecProperty::Qoi))); + } +} diff --git a/crates/ironrdp-testsuite-core/tests/session/rfx.rs b/crates/ironrdp-testsuite-core/tests/session/rfx.rs new file mode 100644 index 00000000..c2e78032 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/session/rfx.rs @@ -0,0 +1,1063 @@ +use ironrdp_graphics::image_processing::PixelFormat; +use ironrdp_pdu::geometry::InclusiveRectangle; +use ironrdp_pdu::ReadCursor; +use ironrdp_session::image::DecodedImage; +use ironrdp_session::rfx::DecodingContext; + +const IMAGE_WIDTH: usize = 64; +const IMAGE_HEIGHT: usize = 64; +const FORMAT_SIZE: usize = 4; + +#[test] +fn decode_decodes_valid_sequence_of_messages() { + let destination = InclusiveRectangle { + left: 0, + top: 0, + right: u16::try_from(IMAGE_WIDTH).unwrap() - 1, + bottom: u16::try_from(IMAGE_HEIGHT).unwrap() - 1, + }; + let data = &mut ReadCursor::new(ENCODED_MESSAGES.as_ref()); + let expected = DECODED_IMAGE.as_ref(); + + let mut image = DecodedImage::new( + PixelFormat::BgrX32, + IMAGE_WIDTH.try_into().unwrap(), + IMAGE_HEIGHT.try_into().unwrap(), + ); + + let mut handler = DecodingContext::default(); + + handler.decode(&mut image, &destination, data).unwrap(); + + assert_eq!(expected, image.data()); +} + +const ENCODED_MESSAGES: [u8; 2970] = [ + /* HEADERS as in 4.2.2 */ + 0xc0, 0xcc, 0x0c, 0x00, 0x00, 0x00, 0xca, 0xac, 0xcc, 0xca, 0x00, 0x01, 0xc3, 0xcc, 0x0d, 0x00, 0x00, 0x00, 0x01, + 0xff, 0x00, 0x40, 0x00, 0x28, 0xa8, 0xc1, 0xcc, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0xc2, 0xcc, 0x0c, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x40, 0x00, 0x40, 0x00, /* FRAME_BEGIN as in 4.2.3 */ + 0xc4, 0xcc, 0x0e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + /* REGION as in 4.2.3 */ + 0xc6, 0xcc, 0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0xcd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x40, 0x00, + 0xc1, 0xca, 0x01, 0x00, /* TILESET as in 4.2.4.1 */ + 0xc7, 0xcc, 0x3e, 0x0b, 0x00, 0x00, 0x01, 0x00, 0xc2, 0xca, 0x00, 0x00, 0x51, 0x50, 0x01, 0x40, 0x01, 0x00, 0x23, + 0x0b, 0x00, 0x00, 0x66, 0x66, 0x77, 0x88, 0x98, 0xc3, 0xca, 0x23, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xae, 0x03, 0xcf, 0x03, 0x93, 0x03, 0xc0, 0x01, 0x01, 0x15, 0x48, 0x99, 0xc7, 0x41, 0xa1, 0x12, 0x68, + 0x11, 0xdc, 0x22, 0x29, 0x74, 0xef, 0xfd, 0x20, 0x92, 0xe0, 0x4e, 0xa8, 0x69, 0x3b, 0xfd, 0x41, 0x83, 0xbf, 0x28, + 0x53, 0x0c, 0x1f, 0xe2, 0x54, 0x0c, 0x77, 0x7c, 0xa3, 0x05, 0x7c, 0x30, 0xd0, 0x9c, 0xe8, 0x09, 0x39, 0x1a, 0x5d, + 0xff, 0xe2, 0x01, 0x22, 0x13, 0x80, 0x90, 0x87, 0xd2, 0x9f, 0xfd, 0xfd, 0x50, 0x09, 0x0d, 0x24, 0xa0, 0x8f, 0xab, + 0xfe, 0x3c, 0x04, 0x84, 0xc6, 0x9c, 0xde, 0xf8, 0x80, 0xc3, 0x22, 0x50, 0xaf, 0x4c, 0x2a, 0x7f, 0xfe, 0xe0, 0x5c, + 0xa9, 0x52, 0x8a, 0x06, 0x7d, 0x3d, 0x09, 0x03, 0x65, 0xa3, 0xaf, 0xd2, 0x61, 0x1f, 0x72, 0x04, 0x50, 0x8d, 0x3e, + 0x16, 0x4a, 0x3f, 0xff, 0xfd, 0x41, 0x42, 0x87, 0x24, 0x37, 0x06, 0x17, 0x2e, 0x56, 0x05, 0x9c, 0x1c, 0xb3, 0x84, + 0x6a, 0xff, 0xfb, 0x43, 0x8b, 0xa3, 0x7a, 0x32, 0x43, 0x28, 0xe1, 0x1f, 0x50, 0x54, 0xfc, 0xca, 0xa5, 0xdf, 0xff, + 0x08, 0x04, 0x48, 0x15, 0x61, 0xd9, 0x76, 0x43, 0xf8, 0x2a, 0x07, 0xe9, 0x65, 0xf7, 0xc6, 0x89, 0x2d, 0x40, 0xa1, + 0xc3, 0x35, 0x8d, 0xf5, 0xed, 0xf5, 0x91, 0xae, 0x2f, 0xcc, 0x01, 0xce, 0x03, 0x48, 0xc0, 0x8d, 0x63, 0xf4, 0xfd, + 0x50, 0x20, 0x2d, 0x0c, 0x9b, 0xb0, 0x8d, 0x13, 0xc0, 0x8a, 0x09, 0x52, 0x1b, 0x02, 0x6e, 0x42, 0x3b, 0xd0, 0x13, + 0x4e, 0x84, 0x01, 0x26, 0x88, 0x6a, 0x04, 0x84, 0x34, 0x2a, 0xa5, 0x00, 0xba, 0x54, 0x48, 0x58, 0xea, 0x54, 0x02, + 0xb4, 0x1d, 0xa7, 0xfa, 0x47, 0x82, 0xec, 0x7a, 0x77, 0xfd, 0x00, 0x92, 0x66, 0x62, 0x04, 0xa6, 0x9b, 0xff, 0xf6, + 0x80, 0xc0, 0x69, 0x01, 0xc2, 0x3e, 0x90, 0x14, 0x20, 0x2f, 0xfc, 0x40, 0x96, 0x59, 0x58, 0x0c, 0xb1, 0x13, 0x68, + 0x20, 0x2e, 0xb5, 0xf5, 0xdf, 0xff, 0xf8, 0xfc, 0x56, 0x88, 0x60, 0x24, 0x53, 0xb5, 0x41, 0x46, 0x5f, 0xf8, 0xf1, + 0x7e, 0xde, 0x4a, 0x08, 0x97, 0xe0, 0x55, 0x03, 0x8f, 0xe5, 0x75, 0x61, 0x03, 0xf2, 0xe1, 0x90, 0x01, 0xa2, 0x8e, + 0x88, 0x04, 0x98, 0x05, 0x93, 0x6b, 0xff, 0xea, 0xc0, 0x60, 0xa1, 0x88, 0x04, 0x49, 0xbf, 0xf7, 0xff, 0x8c, 0xb4, + 0x59, 0x90, 0x80, 0x30, 0x64, 0x53, 0xff, 0xf5, 0xc4, 0x48, 0xda, 0xda, 0xcb, 0x80, 0x38, 0x61, 0x57, 0xb2, 0xaf, + 0x00, 0xe8, 0x7b, 0x46, 0xe6, 0xd8, 0x02, 0x03, 0x8a, 0x06, 0x18, 0x14, 0x32, 0x83, 0xd0, 0x8a, 0xee, 0xbc, 0x81, + 0xb4, 0x28, 0xc4, 0x7f, 0xf9, 0xa1, 0x69, 0x00, 0x91, 0xc5, 0x51, 0xff, 0xfe, 0x3f, 0xe9, 0xf1, 0x70, 0x30, 0x24, + 0x10, 0xa7, 0xcb, 0x1f, 0x8a, 0x24, 0x93, 0xed, 0x83, 0x00, 0x36, 0x20, 0xd1, 0x50, 0xe7, 0xd8, 0xad, 0x58, 0x20, + 0x09, 0x22, 0x80, 0xd0, 0xca, 0x5d, 0x1a, 0xd7, 0xf1, 0x60, 0x75, 0x2a, 0xf2, 0xd7, 0xf8, 0xc0, 0x32, 0x45, 0x86, + 0x00, 0x43, 0x01, 0xfe, 0x80, 0xf7, 0x42, 0x81, 0x74, 0x84, 0x4c, 0xa1, 0x60, 0x4c, 0xcb, 0x14, 0x58, 0x01, 0x4d, + 0x18, 0xa1, 0xaa, 0x47, 0x0e, 0x11, 0x1a, 0x40, 0x7d, 0x41, 0x02, 0xe3, 0x30, 0xcd, 0x33, 0x81, 0x34, 0x06, 0x46, + 0x83, 0xa2, 0x47, 0x1c, 0x04, 0xaa, 0x20, 0x12, 0xa2, 0x8b, 0x81, 0xc4, 0x9c, 0xa0, 0x2e, 0x06, 0x32, 0xf8, 0x86, + 0x85, 0x01, 0xe8, 0x70, 0xf9, 0x46, 0x09, 0x6a, 0xbf, 0xe0, 0xf5, 0xa4, 0xc8, 0x78, 0xe7, 0xd2, 0x97, 0x0b, 0xbc, + 0x3c, 0x97, 0xff, 0xd5, 0x40, 0x94, 0xb2, 0xc1, 0x18, 0x18, 0x11, 0x1f, 0x43, 0xc1, 0x18, 0xc3, 0x83, 0x7f, 0x9a, + 0x31, 0xc4, 0x8e, 0x70, 0x56, 0xda, 0xf6, 0x17, 0xde, 0xd1, 0x02, 0x0d, 0x42, 0x21, 0x13, 0xdc, 0x3a, 0x3c, 0x40, + 0x9e, 0xf4, 0x01, 0x43, 0xea, 0x0c, 0x46, 0x73, 0xa2, 0x7b, 0x0c, 0x80, 0xff, 0xe4, 0xad, 0x2e, 0x09, 0xb4, 0x63, + 0xb0, 0x8c, 0x54, 0x59, 0xfa, 0xac, 0x76, 0x36, 0x10, 0x05, 0xf0, 0x98, 0x88, 0x83, 0x42, 0x00, 0x20, 0x71, 0xcc, + 0xc1, 0xa9, 0x97, 0x3e, 0x5a, 0x0d, 0x04, 0x50, 0x92, 0x23, 0x20, 0x0d, 0x0a, 0x1c, 0x57, 0xd7, 0xff, 0x10, 0xf2, + 0x03, 0x0f, 0x58, 0x1b, 0xa5, 0x11, 0xf8, 0xf1, 0xb4, 0x12, 0xdb, 0x1a, 0x48, 0x56, 0x1f, 0xe3, 0xc7, 0x50, 0xe9, + 0x16, 0xb4, 0xbc, 0xb0, 0x40, 0x93, 0xea, 0xb5, 0x5b, 0x2f, 0xfc, 0x50, 0x0a, 0x6f, 0xcc, 0x25, 0xe0, 0x06, 0xab, + 0x5f, 0x24, 0xfe, 0x8b, 0xcb, 0x42, 0x43, 0x7e, 0x69, 0x02, 0x25, 0xc7, 0x38, 0x00, 0x6e, 0xe5, 0x80, 0xa8, 0xa4, + 0x30, 0x44, 0x15, 0x8f, 0xe9, 0x0c, 0xd3, 0xa6, 0xc2, 0x14, 0x34, 0x4a, 0xfe, 0x03, 0x7f, 0x06, 0xa5, 0x91, 0x02, + 0x54, 0xf1, 0xa1, 0xa1, 0x53, 0xbf, 0x11, 0xf2, 0x8f, 0x83, 0x67, 0x80, 0x09, 0x08, 0x12, 0x3f, 0xfd, 0x44, 0x91, + 0xc2, 0x83, 0x30, 0x50, 0x07, 0x02, 0x82, 0x4d, 0x31, 0x34, 0x06, 0x41, 0x79, 0x6f, 0xf0, 0xcc, 0x03, 0x79, 0x00, + 0x2c, 0x05, 0x24, 0xec, 0x8d, 0x29, 0x15, 0xaf, 0x44, 0xc8, 0xeb, 0x4f, 0xe1, 0xfd, 0xf1, 0x41, 0x48, 0x81, 0x08, + 0xaf, 0xfe, 0x51, 0x48, 0xce, 0xe7, 0xf9, 0xb6, 0x0a, 0x30, 0x83, 0x11, 0xf0, 0x0c, 0x3b, 0xd2, 0xa6, 0x24, 0x24, + 0xef, 0x25, 0xfa, 0x5a, 0x3e, 0x92, 0x3e, 0x79, 0x0e, 0x35, 0x61, 0xc8, 0xaa, 0x1c, 0x2e, 0x9a, 0x27, 0x7f, 0xff, + 0xf0, 0x7d, 0x30, 0x5b, 0xbc, 0x91, 0xff, 0xfe, 0x43, 0x24, 0x28, 0x66, 0xa7, 0x70, 0x99, 0x28, 0x6e, 0x2b, 0x18, + 0x2b, 0xd4, 0xa1, 0x77, 0x3b, 0x96, 0x9f, 0xf7, 0xeb, 0xbe, 0x1f, 0x04, 0x34, 0x75, 0x84, 0x31, 0x42, 0x4c, 0x65, + 0xaa, 0x09, 0x50, 0xa0, 0xc4, 0x51, 0x31, 0xd3, 0x26, 0x3a, 0x1b, 0xf4, 0x6e, 0x4a, 0x4e, 0x17, 0x25, 0x84, 0x78, + 0x7d, 0x2c, 0x3f, 0x46, 0x18, 0xca, 0x5f, 0xf9, 0xe5, 0x38, 0x2f, 0xd8, 0x71, 0x94, 0x94, 0xe2, 0xcc, 0xa3, 0x15, + 0xb0, 0xda, 0xa9, 0xcb, 0x58, 0xe4, 0x18, 0x77, 0x93, 0x8a, 0x51, 0xc6, 0x23, 0xc4, 0x4e, 0x6d, 0xd9, 0x14, 0x1e, + 0x9b, 0x8d, 0xbc, 0xcb, 0x9d, 0xc4, 0x18, 0x05, 0xf5, 0xa9, 0x29, 0xf8, 0x6d, 0x29, 0x38, 0xc7, 0x44, 0xe5, 0x3a, + 0xcd, 0xba, 0x61, 0x98, 0x4a, 0x57, 0x02, 0x96, 0x42, 0x02, 0xd9, 0x37, 0x11, 0xde, 0x2d, 0xd4, 0x3f, 0xfe, 0x61, + 0xe7, 0x33, 0xd7, 0x89, 0x4a, 0xdd, 0xb0, 0x34, 0x47, 0xf4, 0xdc, 0xad, 0xaa, 0xc9, 0x9d, 0x7e, 0x6d, 0x4b, 0xcc, + 0xdc, 0x17, 0x89, 0x57, 0xfd, 0xbb, 0x37, 0x75, 0x47, 0x5a, 0xec, 0x2c, 0x6e, 0x3c, 0x15, 0x92, 0x54, 0x64, 0x2c, + 0xab, 0x9e, 0xab, 0x2b, 0xdd, 0x3c, 0x66, 0xa0, 0x8f, 0x47, 0x5e, 0x93, 0x1a, 0x37, 0x16, 0xf4, 0x89, 0x23, 0x00, + 0x00, 0xb0, 0x33, 0x56, 0xfa, 0x14, 0x1e, 0xff, 0x48, 0x7a, 0x7e, 0x0f, 0x10, 0x1f, 0xf4, 0x91, 0xc8, 0x10, 0x56, + 0x84, 0xff, 0x08, 0xec, 0xb4, 0xac, 0x0e, 0x0f, 0xff, 0xad, 0xc5, 0xe0, 0x1a, 0x2f, 0x82, 0x04, 0x9f, 0x91, 0xc2, + 0x0e, 0xfe, 0x48, 0x36, 0x79, 0x01, 0x42, 0x14, 0xff, 0xfe, 0x30, 0xf0, 0x08, 0x18, 0xf1, 0x81, 0x45, 0x9a, 0x60, + 0xc1, 0x79, 0xf0, 0x14, 0x12, 0x10, 0xce, 0xea, 0x31, 0x5a, 0xff, 0xfc, 0x20, 0x13, 0x82, 0x2f, 0xc9, 0x02, 0x1f, + 0x81, 0xcb, 0x00, 0xe1, 0x10, 0xd2, 0xb4, 0xbe, 0x87, 0xff, 0xb0, 0x1e, 0x27, 0x81, 0xb7, 0x04, 0x06, 0x3c, 0xc2, + 0x04, 0xf6, 0x06, 0x0e, 0x28, 0xbc, 0x40, 0xbf, 0x12, 0x1e, 0x86, 0xd4, 0x6a, 0x7f, 0x18, 0x1b, 0x96, 0x85, 0x4c, + 0x16, 0x80, 0xdf, 0x2c, 0xa5, 0x8d, 0x86, 0xa3, 0x4a, 0x8a, 0xb4, 0x1b, 0xa1, 0x38, 0xa9, 0xd5, 0xff, 0xff, 0xea, + 0x06, 0x20, 0xd2, 0x95, 0x1e, 0xf4, 0x2f, 0xb2, 0x12, 0x0e, 0x61, 0x78, 0x4a, 0x17, 0x52, 0x5d, 0xe4, 0x25, 0x1f, + 0xfe, 0xc0, 0xb3, 0x1f, 0xff, 0xff, 0xec, 0x02, 0x82, 0x80, 0x90, 0x41, 0x88, 0xde, 0x48, 0x2c, 0x42, 0x52, 0x0b, + 0x2f, 0x43, 0x7e, 0x50, 0x78, 0xf2, 0x67, 0x78, 0x41, 0x34, 0x3d, 0xc8, 0x0f, 0x67, 0xa1, 0xeb, 0x21, 0xfe, 0xc0, + 0x1f, 0x22, 0x60, 0x41, 0x6c, 0x00, 0x92, 0x4b, 0x60, 0x10, 0xd0, 0x0d, 0x01, 0x35, 0x05, 0x0e, 0x87, 0xa2, 0xa0, + 0x5d, 0x1f, 0xa3, 0xaf, 0x7f, 0xf1, 0xbe, 0x8f, 0xcd, 0xa5, 0x00, 0x1c, 0x10, 0x40, 0x15, 0x76, 0x81, 0x05, 0xef, + 0xee, 0x00, 0x60, 0x84, 0x00, 0x99, 0x40, 0x4a, 0x82, 0x17, 0xe9, 0xfc, 0xc4, 0x7f, 0xff, 0xfd, 0x04, 0x80, 0x06, + 0x06, 0xdc, 0xaf, 0xa7, 0x7e, 0x94, 0x75, 0x74, 0x01, 0x00, 0xe0, 0x91, 0x00, 0x85, 0x7f, 0x8e, 0xd6, 0x0b, 0x20, + 0x21, 0x30, 0xca, 0x62, 0x8e, 0x07, 0x04, 0xe9, 0x45, 0x40, 0x5f, 0x47, 0x4a, 0x30, 0x15, 0x41, 0xcb, 0xdf, 0xff, + 0xfc, 0xbf, 0xc3, 0xb4, 0x46, 0x6a, 0x01, 0x40, 0xd0, 0xa7, 0x34, 0x18, 0x24, 0x1c, 0x2a, 0x45, 0xfe, 0xa8, 0x05, + 0x08, 0x61, 0xfd, 0xa8, 0x80, 0x71, 0x01, 0x25, 0x9c, 0xc1, 0x47, 0x17, 0x37, 0x02, 0x7a, 0x15, 0xff, 0xf3, 0x01, + 0x45, 0x7f, 0xd6, 0x80, 0x60, 0x83, 0x67, 0xf8, 0x9d, 0x2f, 0xf4, 0xdd, 0x8c, 0x30, 0x01, 0x51, 0x42, 0xbc, 0x43, + 0x7a, 0x6b, 0x9f, 0x84, 0x1e, 0x00, 0x48, 0xc1, 0xe0, 0xb7, 0xe0, 0x7e, 0x99, 0xf2, 0x4a, 0xe9, 0x40, 0x02, 0x81, + 0xc3, 0x00, 0x24, 0x3a, 0xc5, 0x52, 0x0f, 0x91, 0xc8, 0x68, 0x25, 0x40, 0x99, 0xa4, 0x25, 0x1a, 0x04, 0xd0, 0xa2, + 0x91, 0xdd, 0xeb, 0x93, 0x00, 0x21, 0x49, 0x24, 0x8b, 0x40, 0x75, 0x38, 0x14, 0xa1, 0xfd, 0x3f, 0x88, 0x25, 0xbf, + 0x32, 0x00, 0xe3, 0x19, 0xfc, 0xb9, 0xf8, 0x6f, 0x81, 0xc0, 0x01, 0xb3, 0x93, 0x20, 0x09, 0x08, 0x25, 0x84, 0xe1, + 0x34, 0xd4, 0x1b, 0x48, 0x88, 0x11, 0xa0, 0x15, 0x59, 0xd7, 0x07, 0x81, 0x81, 0x3b, 0xa1, 0x40, 0x2e, 0x2f, 0x48, + 0x70, 0x09, 0xc4, 0x76, 0x49, 0x0f, 0x2e, 0x50, 0x2e, 0x46, 0x19, 0xa4, 0x16, 0xa2, 0x1b, 0x84, 0xa2, 0x89, 0x58, + 0xfc, 0x4f, 0x3f, 0x40, 0x90, 0x4c, 0xa3, 0x01, 0x32, 0x09, 0x02, 0x80, 0x9c, 0x91, 0x13, 0x2c, 0xba, 0xde, 0x5d, + 0x99, 0xf2, 0xff, 0xff, 0x3d, 0x5a, 0x1f, 0xa9, 0x02, 0x90, 0x8f, 0xf3, 0x08, 0xbd, 0x01, 0xf8, 0xd0, 0x2a, 0x95, + 0x41, 0x0c, 0x40, 0x0a, 0x20, 0xc4, 0xd4, 0xcc, 0x6b, 0x0f, 0xf0, 0x80, 0xb1, 0x5d, 0x28, 0x3d, 0x08, 0xc2, 0xf8, + 0x31, 0x02, 0x49, 0x88, 0x14, 0x28, 0xed, 0xe8, 0x86, 0x3b, 0x00, 0x9f, 0x95, 0x06, 0x37, 0x15, 0xa4, 0x59, 0xc8, + 0x80, 0xb6, 0x10, 0xf0, 0xe5, 0xb8, 0x18, 0x00, 0x56, 0x1c, 0xff, 0x95, 0x21, 0x0e, 0x7f, 0x2b, 0xc5, 0x08, 0x59, + 0x10, 0xe1, 0x46, 0x31, 0x8d, 0xec, 0xe0, 0xa1, 0x99, 0xbb, 0x21, 0xff, 0xfe, 0x30, 0x10, 0xd0, 0x05, 0xe3, 0x08, + 0x50, 0xfc, 0xf3, 0x0e, 0x00, 0x8d, 0x68, 0x8e, 0x07, 0xa6, 0x80, 0x34, 0x42, 0xed, 0x1f, 0x88, 0x00, 0xf0, 0x8a, + 0x21, 0xae, 0xf7, 0xfb, 0x80, 0x28, 0x86, 0x0f, 0xff, 0xff, 0x82, 0xea, 0x47, 0x95, 0x91, 0xe0, 0x04, 0x01, 0x44, + 0x0c, 0x29, 0xff, 0x0e, 0x33, 0xe8, 0xc0, 0x54, 0x04, 0x23, 0xfc, 0x81, 0x5b, 0xf0, 0x3c, 0x07, 0x10, 0x70, 0x30, + 0xd8, 0x21, 0x6f, 0xef, 0xde, 0x46, 0x09, 0x43, 0xfa, 0x5f, 0xff, 0x0d, 0x72, 0x30, 0xdd, 0x00, 0xdb, 0xe4, 0x48, + 0x24, 0x97, 0x08, 0x46, 0xb1, 0x49, 0xc4, 0x4d, 0x80, 0x12, 0x60, 0xff, 0xa4, 0xa6, 0xff, 0xf6, 0x8c, 0x00, 0x40, + 0x05, 0x02, 0xb4, 0x0f, 0xf0, 0x3e, 0xfc, 0x84, 0x38, 0x81, 0x94, 0x8b, 0xfe, 0x49, 0xef, 0xc0, 0x10, 0x49, 0x88, + 0x28, 0xa2, 0x1c, 0x2a, 0x8b, 0x64, 0xd4, 0x86, 0xd7, 0xff, 0xff, 0xff, 0xeb, 0x91, 0x6b, 0x11, 0x10, 0x00, 0x69, + 0x4c, 0xbf, 0xb4, 0x1c, 0xd8, 0x00, 0x07, 0x16, 0x80, 0x60, 0x0a, 0x1c, 0x82, 0x42, 0x27, 0x82, 0x43, 0xc9, 0x0a, + 0x64, 0x20, 0x5a, 0x5f, 0x4e, 0xbf, 0x8c, 0x38, 0x82, 0x36, 0x02, 0x07, 0x72, 0x79, 0x07, 0x23, 0xb4, 0xbb, 0x57, + 0x5f, 0xe8, 0x04, 0xdd, 0x39, 0xe9, 0x07, 0x95, 0xbe, 0x04, 0x2b, 0xdd, 0x8e, 0x22, 0xdc, 0x14, 0x2c, 0x61, 0xa3, + 0xa9, 0xcd, 0x4f, 0x82, 0x5d, 0xa0, 0x44, 0xdf, 0xf4, 0x96, 0xff, 0xf5, 0x2b, 0xff, 0xfe, 0x01, 0x19, 0xd2, 0xa2, + 0x9e, 0x43, 0xa5, 0x7f, 0xf0, 0x4c, 0x4c, 0x2b, 0x3c, 0x33, 0xe2, 0x55, 0xff, 0x04, 0x06, 0x29, 0x2c, 0x0d, 0x22, + 0x5d, 0x7c, 0x93, 0xba, 0x18, 0xaf, 0xf9, 0x32, 0xa6, 0xc3, 0x99, 0x46, 0x79, 0xe3, 0x06, 0xa6, 0x38, 0x8b, 0x92, + 0x22, 0x4b, 0xdb, 0x1b, 0x36, 0x20, 0xb0, 0x6c, 0x20, 0xce, 0x37, 0x42, 0xe1, 0x66, 0xd4, 0x49, 0x34, 0x42, 0x8b, + 0xfa, 0x9c, 0x12, 0x99, 0xdc, 0x06, 0x87, 0xfa, 0x46, 0xf8, 0x2f, 0x04, 0xa9, 0xd8, 0x82, 0x07, 0xa6, 0x30, 0x0f, + 0xc0, 0xdf, 0x35, 0xe8, 0x90, 0xf0, 0xff, 0xff, 0xa8, 0xe0, 0xd7, 0x02, 0x60, 0x1a, 0xc3, 0x20, 0x28, 0xa2, 0x31, + 0x29, 0x3c, 0xeb, 0x04, 0xa5, 0xdd, 0x48, 0x0e, 0x82, 0xa4, 0xb6, 0x56, 0x22, 0x06, 0x57, 0xe0, 0xda, 0x10, 0x27, + 0x31, 0x0e, 0x11, 0x77, 0xfe, 0x02, 0x60, 0x16, 0x48, 0x81, 0x8c, 0x0d, 0x05, 0x17, 0x7f, 0xcb, 0xbb, 0x7e, 0x25, + 0x2a, 0x41, 0xfd, 0x8a, 0x7f, 0xc9, 0x36, 0x7c, 0xe0, 0x98, 0x7e, 0x92, 0xef, 0x7e, 0x06, 0x03, 0x13, 0x3e, 0x20, + 0x3a, 0xbf, 0x4c, 0xc3, 0x0f, 0x2e, 0x80, 0x74, 0xbf, 0x39, 0x3c, 0xf0, 0xa6, 0xb2, 0xe9, 0x3f, 0x41, 0x55, 0x1f, + 0x2c, 0xf5, 0xd2, 0x7e, 0x8c, 0xae, 0x4e, 0xaa, 0x61, 0x3c, 0xbc, 0x3f, 0xc4, 0xc7, 0x36, 0xdc, 0x23, 0xc8, 0xb8, + 0x52, 0xe2, 0x8a, 0x80, 0x18, 0x00, 0x00, 0xb2, 0x46, 0xa2, 0x56, 0x0d, 0x12, 0x94, 0xaa, 0xbd, 0x01, 0x07, 0xff, + 0xfa, 0x34, 0x0c, 0x5f, 0xf8, 0x0c, 0x12, 0x50, 0xaf, 0xd6, 0xd1, 0x89, 0x40, 0xa4, 0xff, 0xe0, 0xce, 0xc4, 0x49, + 0x25, 0x9d, 0xc1, 0xff, 0x7e, 0x60, 0x24, 0x5d, 0xcc, 0x10, 0xc0, 0xbe, 0x5a, 0x12, 0xd3, 0xc3, 0xfe, 0x2d, 0x40, + 0x7c, 0x28, 0x9e, 0x71, 0x01, 0xd2, 0x6e, 0x86, 0x0b, 0xc8, 0xf2, 0x9b, 0x45, 0x08, 0x4c, 0x04, 0x52, 0x7e, 0xf2, + 0x7e, 0xd9, 0xcc, 0x0b, 0x1c, 0x20, 0x80, 0xae, 0xaf, 0xfe, 0xb0, 0x6d, 0x23, 0xf2, 0x41, 0xe3, 0x2e, 0x20, 0x11, + 0x4b, 0x74, 0x89, 0xdd, 0xff, 0xa8, 0x38, 0xa3, 0x95, 0x82, 0x15, 0xf0, 0xd0, 0xd5, 0xf1, 0x92, 0x8e, 0xee, 0xc0, + 0x26, 0x81, 0xe9, 0x47, 0xff, 0xee, 0x0d, 0x20, 0x34, 0x31, 0x3a, 0xef, 0x40, 0xb2, 0x29, 0x47, 0x19, 0x7f, 0x04, + 0x27, 0xf1, 0x90, 0x85, 0x09, 0x86, 0x7d, 0x42, 0xe2, 0x54, 0x5d, 0x5f, 0xe8, 0x0e, 0xd0, 0x2c, 0xaa, 0x16, 0xbf, + 0x04, 0xa7, 0xf8, 0xa2, 0x46, 0x0b, 0x08, 0x7a, 0x79, 0xe9, 0x28, 0x62, 0x7c, 0x33, 0xf4, 0x0b, 0x14, 0x82, 0xfa, + 0x61, 0xeb, 0xc1, 0xff, 0x4c, 0xa4, 0x11, 0x7f, 0x03, 0x68, 0x44, 0xc1, 0x1f, 0x81, 0x3a, 0x6c, 0x77, 0x95, 0x02, + 0x2b, 0x53, 0x80, 0xe5, 0x10, 0x1e, 0x90, 0xe8, 0xfd, 0x1f, 0xa6, 0x40, 0x0b, 0x13, 0xff, 0x4e, 0x4d, 0x7f, 0x52, + 0xe8, 0xaf, 0x9a, 0xc1, 0x80, 0x0f, 0x0a, 0x14, 0x02, 0x3c, 0xc0, 0x09, 0x13, 0xe7, 0xdc, 0xc0, 0x1a, 0x28, 0xa0, + 0xe4, 0x83, 0x8e, 0x03, 0x88, 0xd5, 0xaf, 0x1a, 0xbd, 0x91, 0x00, 0xb7, 0x4e, 0xba, 0xdf, 0xf8, 0xdb, 0xcc, 0x02, + 0x43, 0xc4, 0x14, 0x2a, 0x3f, 0xc8, 0x0d, 0x09, 0x1c, 0x44, 0xf4, 0x01, 0x3c, 0xca, 0x28, 0x56, 0x80, 0xa6, 0x85, + 0x00, 0xea, 0x3e, 0x8f, 0xeb, 0x9f, 0xfc, 0x6e, 0x07, 0xc4, 0xe0, 0x30, 0x78, 0xa0, 0x1e, 0x6f, 0x54, 0x78, 0x51, + 0xff, 0x56, 0x4a, 0x01, 0x47, 0x02, 0x4c, 0x21, 0x3b, 0xfb, 0x90, 0x0a, 0xcc, 0x1d, 0xd2, 0x47, 0xff, 0xfc, 0x70, + 0x18, 0x22, 0xc0, 0xb9, 0x2f, 0xe9, 0x7f, 0x91, 0xd3, 0x66, 0x2f, 0x80, 0x2c, 0x24, 0xa7, 0xfa, 0x84, 0x51, 0xab, + 0x6b, 0x72, 0x00, 0xab, 0x33, 0x04, 0xcf, 0x43, 0xff, 0x17, 0x51, 0x84, 0x0c, 0x01, 0x50, 0x10, 0x8f, 0x90, 0x34, + 0x41, 0x44, 0x84, 0x8e, 0x08, 0x19, 0x04, 0x48, 0x50, 0x84, 0x38, 0x3d, 0x02, 0x52, 0xf9, 0x7c, 0xd2, 0xd0, 0x1f, + 0x13, 0x42, 0xa0, 0x21, 0x41, 0xc4, 0x02, 0x02, 0x3d, 0x09, 0xc8, 0xfd, 0x60, 0x7d, 0x35, 0x4f, 0x7f, 0xff, 0xf9, + 0x97, 0x6a, 0xd8, 0x00, 0xc3, 0x83, 0x00, 0x09, 0x50, 0x4b, 0x90, 0x8a, 0xc7, 0x94, 0x4d, 0x47, 0xc1, 0x62, 0x32, + 0x28, 0x24, 0x09, 0x52, 0x2e, 0x2e, 0x1c, 0x96, 0x44, 0xa0, 0x09, 0xc8, 0xce, 0x64, 0xa9, 0x1c, 0x19, 0x0e, 0x52, + 0x3e, 0x3e, 0x19, 0x93, 0xa0, 0x36, 0x26, 0x22, 0x08, 0x9a, 0x00, 0xdd, 0x66, 0x3a, 0x93, 0xd5, 0x89, 0xd1, 0x40, + 0x06, 0xd4, 0xa8, 0x22, 0x73, 0x7b, 0x3d, 0x3f, 0xe3, 0x04, 0x94, 0xff, 0xff, 0xff, 0xff, 0x0c, 0x56, 0x77, 0xac, + 0xe0, 0xc4, 0x06, 0x1f, 0xb8, 0xa5, 0x80, 0xfd, 0x68, 0x1c, 0x32, 0x16, 0x03, 0xde, 0x71, 0x2a, 0x3d, 0x14, 0x19, + 0xbe, 0xc2, 0x88, 0xd9, 0x24, 0x92, 0x5f, 0xc5, 0x90, 0x0a, 0x85, 0xc2, 0x3f, 0x87, 0x03, 0xa8, 0x26, 0x17, 0xc4, + 0x06, 0x86, 0x12, 0x87, 0x76, 0x0a, 0x48, 0x16, 0xed, 0x96, 0x93, 0xec, 0x1b, 0x30, 0x73, 0xe8, 0x1a, 0x3f, 0xff, + 0x4d, 0xce, 0x40, 0xf3, 0x0c, 0x51, 0x4b, 0x84, 0x9e, 0x67, 0x2b, 0x15, 0x40, 0x1a, 0xa0, 0xfc, 0x10, 0x0f, 0xd8, + 0x81, 0x35, 0x87, 0xff, 0x98, 0x0f, 0x40, 0x00, 0xba, 0xc0, 0x71, 0xe2, 0x00, 0x18, 0x28, 0xb3, 0x82, 0xcc, 0x80, + 0x6a, 0xa0, 0x43, 0xff, 0x2d, 0xd6, 0x04, 0x8a, 0x68, 0xff, 0xff, 0xff, 0xfc, 0x1a, 0xf3, 0x1a, 0x2a, 0x06, 0xc0, + 0x01, 0x40, 0x0c, 0x30, 0xc1, 0xd0, 0xd7, 0x4f, 0xcb, 0x74, 0x1f, 0x07, 0xd3, 0xb4, 0x0d, 0x88, 0x98, 0xea, 0xda, + 0x9f, 0xce, 0x2b, 0x3c, 0x55, 0xb3, 0x40, 0x14, 0xff, 0xff, 0xff, 0xea, 0xdb, 0x9b, 0x92, 0xd8, 0x68, 0x08, 0x0b, + 0x41, 0x09, 0x26, 0x40, 0x8c, 0xf1, 0xb0, 0x9a, 0x98, 0xc0, 0x80, 0x8b, 0xf0, 0x3d, 0xe7, 0xec, 0x19, 0x68, 0x21, + 0x03, 0x29, 0x7f, 0xe1, 0x6d, 0x4c, 0x0f, 0x01, 0xd1, 0x51, 0x01, 0x1a, 0x50, 0x2a, 0x59, 0x27, 0x80, 0xc1, 0x6e, + 0x33, 0xf1, 0x80, 0xe1, 0x49, 0x08, 0xe9, 0x17, 0xff, 0xff, 0xff, 0x80, 0x5a, 0x10, 0x10, 0x36, 0x5e, 0xca, 0xf8, + 0x3a, 0x00, 0x1e, 0xb0, 0x06, 0x84, 0x01, 0xf3, 0x07, 0x1b, 0x4a, 0xc0, 0x1e, 0x21, 0x43, 0x8e, 0xa5, 0x55, 0x77, + 0xc7, 0x65, 0x7c, 0xc2, 0xdf, 0x5e, 0x0c, 0x42, 0x20, 0xd2, 0x48, 0x61, 0xc8, 0x1c, 0x65, 0xf8, 0xfe, 0x4c, 0x88, + 0x71, 0x1f, 0x82, 0x50, 0x81, 0xa3, 0x54, 0x09, 0x13, 0x28, 0x52, 0xf5, 0xe0, 0x82, 0xc3, 0x06, 0x7f, 0xfa, 0x2c, + 0xcf, 0xf8, 0xf4, 0x7f, 0xff, 0xfd, 0x01, 0x49, 0xa4, 0xb8, 0xde, 0x62, 0x84, 0xfe, 0xed, 0x65, 0x1f, 0x3c, 0x3c, + 0xb2, 0x50, 0x76, 0x30, 0x5b, 0x03, 0xc0, 0x08, 0xa6, 0x64, 0x90, 0xc8, 0xcd, 0x14, 0x6e, 0x69, 0x46, 0x7a, 0xc6, + 0x1c, 0x87, 0xd7, 0x48, 0x7b, 0x49, 0x05, 0x2d, 0x5e, 0x7f, 0xcb, 0x67, 0xf0, 0xd9, 0x0d, 0x1e, 0x9e, 0x53, 0xb7, + 0x64, 0xa5, 0xa5, 0x10, 0x39, 0x06, 0x11, 0x3f, 0xb1, 0xa9, 0xa6, 0xe8, 0x4d, 0x47, 0x77, 0xda, 0x43, 0x76, 0x89, + 0x45, 0x09, 0x70, 0xc2, 0x38, 0x0f, 0x09, 0x6f, 0xe7, 0x2d, 0x82, 0x35, 0x07, 0xfe, 0x64, 0x18, 0x2e, 0xb8, 0x04, + 0x42, 0x54, 0x80, 0x43, 0x12, 0x6c, 0x9a, 0x55, 0xc9, 0x0a, 0xa0, 0x79, 0x47, 0x52, 0x65, 0x2a, 0xff, 0x50, 0x11, + 0xc9, 0x4e, 0xfe, 0x5b, 0x30, 0xa4, 0xe8, 0x30, 0x63, 0xff, 0x21, 0x12, 0x1b, 0xdc, 0x1c, 0x01, 0x41, 0x51, 0x1f, + 0xff, 0xfa, 0xc3, 0xe3, 0x55, 0xf1, 0x66, 0xe2, 0xd5, 0x78, 0x5e, 0xfa, 0x4d, 0xf2, 0x61, 0x01, 0x26, 0x15, 0xa9, + 0xf9, 0xd9, 0x32, 0x41, 0x90, 0x36, 0x4e, 0xae, 0xe3, 0x0b, 0x16, 0x56, 0x8c, 0x6e, 0x42, 0x5d, 0xd8, 0x1e, 0xfe, + 0x1d, 0x40, 0x3a, 0x50, 0x9f, 0x09, 0x14, 0xeb, 0x6e, 0x48, 0x7a, 0x91, 0x88, 0x7b, 0x7d, 0x8f, 0x72, 0x42, 0x39, + 0xb0, 0x1c, 0x65, 0x18, 0x23, 0x8b, 0x60, 0x30, 0x00, /* FRAME_END as in 4.2.3 */ + 0xc5, 0xcc, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, +]; + +const DECODED_IMAGE: [u8; IMAGE_WIDTH * IMAGE_HEIGHT * FORMAT_SIZE] = [ + 0xDE, 0x9B, 0x22, 0xFF, 0xE0, 0x9D, 0x23, 0xFF, 0xE1, 0x9E, 0x25, 0xFF, 0xE8, 0xA5, 0x2B, 0xFF, 0xDF, 0x9B, 0x22, + 0xFF, 0xDF, 0x9C, 0x22, 0xFF, 0xE0, 0x9C, 0x22, 0xFF, 0xDF, 0x9C, 0x22, 0xFF, 0xDF, 0x9B, 0x21, 0xFF, 0xDF, 0x9B, + 0x22, 0xFF, 0xDF, 0x9B, 0x23, 0xFF, 0xDF, 0x9B, 0x23, 0xFF, 0xDF, 0x9C, 0x24, 0xFF, 0xE2, 0x9B, 0x21, 0xFF, 0xE5, + 0x9B, 0x1D, 0xFF, 0xE1, 0x9A, 0x1F, 0xFF, 0xDD, 0x98, 0x21, 0xFF, 0xDE, 0x99, 0x21, 0xFF, 0xDE, 0x99, 0x20, 0xFF, + 0xDF, 0x9A, 0x1F, 0xFF, 0xE0, 0x9A, 0x1F, 0xFF, 0xE0, 0x99, 0x1E, 0xFF, 0xDF, 0x99, 0x1D, 0xFF, 0xDF, 0x98, 0x1C, + 0xFF, 0xDF, 0x97, 0x1B, 0xFF, 0xDC, 0x95, 0x1E, 0xFF, 0xD8, 0x93, 0x21, 0xFF, 0xDC, 0x93, 0x1F, 0xFF, 0xE0, 0x93, + 0x1C, 0xFF, 0xDC, 0x94, 0x1A, 0xFF, 0xD8, 0x95, 0x18, 0xFF, 0xDB, 0x91, 0x1C, 0xFF, 0xDE, 0x8E, 0x1F, 0xFF, 0xDE, + 0x90, 0x1A, 0xFF, 0xDE, 0x93, 0x16, 0xFF, 0xDF, 0x92, 0x17, 0xFF, 0xDF, 0x91, 0x18, 0xFF, 0xDF, 0x90, 0x17, 0xFF, + 0xDE, 0x8F, 0x17, 0xFF, 0xDE, 0x8E, 0x16, 0xFF, 0xDE, 0x8C, 0x15, 0xFF, 0xDD, 0x8C, 0x14, 0xFF, 0xDB, 0x8C, 0x13, + 0xFF, 0xDA, 0x8C, 0x12, 0xFF, 0xD9, 0x8C, 0x11, 0xFF, 0xD9, 0x8B, 0x11, 0xFF, 0xD9, 0x89, 0x11, 0xFF, 0xDA, 0x88, + 0x11, 0xFF, 0xDA, 0x87, 0x12, 0xFF, 0xDA, 0x86, 0x11, 0xFF, 0xDA, 0x86, 0x10, 0xFF, 0xD9, 0x85, 0x10, 0xFF, 0xD9, + 0x84, 0x0F, 0xFF, 0xD9, 0x83, 0x0E, 0xFF, 0xD8, 0x83, 0x0E, 0xFF, 0xD8, 0x82, 0x0D, 0xFF, 0xD8, 0x81, 0x0C, 0xFF, + 0xD7, 0x80, 0x0C, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xD6, 0x7F, 0x0D, 0xFF, 0xD6, 0x7E, 0x0D, 0xFF, 0xD6, 0x7E, 0x0D, + 0xFF, 0xD6, 0x7E, 0x0D, 0xFF, 0xD6, 0x7E, 0x0D, 0xFF, 0xE0, 0x9F, 0x24, 0xFF, 0xE1, 0xA0, 0x27, 0xFF, 0xE2, 0xA2, + 0x29, 0xFF, 0xE5, 0xA4, 0x2A, 0xFF, 0xE0, 0x9E, 0x24, 0xFF, 0xE1, 0x9E, 0x24, 0xFF, 0xE1, 0x9E, 0x24, 0xFF, 0xE1, + 0x9E, 0x23, 0xFF, 0xE1, 0x9D, 0x23, 0xFF, 0xE1, 0x9D, 0x23, 0xFF, 0xE1, 0x9D, 0x24, 0xFF, 0xE1, 0x9D, 0x24, 0xFF, + 0xE1, 0x9D, 0x25, 0xFF, 0xE1, 0x9D, 0x23, 0xFF, 0xE2, 0x9C, 0x22, 0xFF, 0xE0, 0x9C, 0x22, 0xFF, 0xDF, 0x9B, 0x22, + 0xFF, 0xE0, 0x9B, 0x21, 0xFF, 0xE1, 0x9B, 0x20, 0xFF, 0xE1, 0x9B, 0x20, 0xFF, 0xE1, 0x9B, 0x1F, 0xFF, 0xDF, 0x9A, + 0x20, 0xFF, 0xDE, 0x99, 0x20, 0xFF, 0xDE, 0x98, 0x1E, 0xFF, 0xDF, 0x97, 0x1D, 0xFF, 0xDF, 0x97, 0x1D, 0xFF, 0xDF, + 0x96, 0x1E, 0xFF, 0xDF, 0x95, 0x1D, 0xFF, 0xDE, 0x94, 0x1C, 0xFF, 0xDF, 0x94, 0x1C, 0xFF, 0xE0, 0x93, 0x1B, 0xFF, + 0xE0, 0x93, 0x1C, 0xFF, 0xE0, 0x92, 0x1D, 0xFF, 0xDE, 0x93, 0x1B, 0xFF, 0xDC, 0x94, 0x19, 0xFF, 0xDE, 0x93, 0x19, + 0xFF, 0xE0, 0x92, 0x19, 0xFF, 0xDF, 0x91, 0x19, 0xFF, 0xDF, 0x90, 0x18, 0xFF, 0xDF, 0x8F, 0x17, 0xFF, 0xDF, 0x8E, + 0x17, 0xFF, 0xDE, 0x8E, 0x16, 0xFF, 0xDD, 0x8D, 0x15, 0xFF, 0xDC, 0x8D, 0x13, 0xFF, 0xDB, 0x8D, 0x12, 0xFF, 0xDB, + 0x8C, 0x12, 0xFF, 0xDB, 0x8B, 0x12, 0xFF, 0xDB, 0x89, 0x12, 0xFF, 0xDB, 0x88, 0x12, 0xFF, 0xDB, 0x87, 0x11, 0xFF, + 0xDB, 0x87, 0x11, 0xFF, 0xDB, 0x86, 0x10, 0xFF, 0xDB, 0x85, 0x0F, 0xFF, 0xDA, 0x84, 0x0E, 0xFF, 0xD9, 0x83, 0x0D, + 0xFF, 0xD9, 0x83, 0x0D, 0xFF, 0xD9, 0x83, 0x0D, 0xFF, 0xD8, 0x82, 0x0D, 0xFF, 0xD8, 0x81, 0x0D, 0xFF, 0xD7, 0x80, + 0x0D, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xD7, 0x7F, 0x0D, 0xFF, 0xE2, + 0xA2, 0x27, 0xFF, 0xE3, 0xA4, 0x2A, 0xFF, 0xE3, 0xA5, 0x2D, 0xFF, 0xE3, 0xA3, 0x29, 0xFF, 0xE2, 0xA1, 0x26, 0xFF, + 0xE2, 0xA1, 0x25, 0xFF, 0xE2, 0xA1, 0x25, 0xFF, 0xE2, 0xA0, 0x25, 0xFF, 0xE2, 0xA0, 0x24, 0xFF, 0xE2, 0x9F, 0x25, + 0xFF, 0xE3, 0x9F, 0x25, 0xFF, 0xE3, 0x9E, 0x25, 0xFF, 0xE3, 0x9E, 0x26, 0xFF, 0xE1, 0x9E, 0x26, 0xFF, 0xDE, 0x9D, + 0x27, 0xFF, 0xDF, 0x9D, 0x24, 0xFF, 0xE1, 0x9E, 0x22, 0xFF, 0xE2, 0x9D, 0x21, 0xFF, 0xE3, 0x9D, 0x20, 0xFF, 0xE3, + 0x9D, 0x20, 0xFF, 0xE3, 0x9C, 0x20, 0xFF, 0xDF, 0x9B, 0x22, 0xFF, 0xDC, 0x99, 0x24, 0xFF, 0xDE, 0x98, 0x21, 0xFF, + 0xE0, 0x98, 0x1F, 0xFF, 0xE3, 0x99, 0x1D, 0xFF, 0xE7, 0x9A, 0x1B, 0xFF, 0xE1, 0x98, 0x1B, 0xFF, 0xDC, 0x96, 0x1C, + 0xFF, 0xE2, 0x94, 0x1D, 0xFF, 0xE9, 0x92, 0x1F, 0xFF, 0xE5, 0x94, 0x1D, 0xFF, 0xE2, 0x96, 0x1A, 0xFF, 0xDE, 0x95, + 0x1B, 0xFF, 0xDA, 0x95, 0x1D, 0xFF, 0xDD, 0x94, 0x1C, 0xFF, 0xE0, 0x93, 0x1A, 0xFF, 0xE0, 0x92, 0x1A, 0xFF, 0xE0, + 0x91, 0x19, 0xFF, 0xDF, 0x91, 0x19, 0xFF, 0xDF, 0x90, 0x18, 0xFF, 0xDE, 0x8F, 0x17, 0xFF, 0xDE, 0x8F, 0x16, 0xFF, + 0xDD, 0x8E, 0x15, 0xFF, 0xDD, 0x8E, 0x14, 0xFF, 0xDC, 0x8D, 0x14, 0xFF, 0xDC, 0x8C, 0x13, 0xFF, 0xDC, 0x8B, 0x12, + 0xFF, 0xDB, 0x8A, 0x12, 0xFF, 0xDC, 0x89, 0x11, 0xFF, 0xDC, 0x88, 0x11, 0xFF, 0xDC, 0x87, 0x10, 0xFF, 0xDC, 0x86, + 0x10, 0xFF, 0xDB, 0x84, 0x0E, 0xFF, 0xD9, 0x83, 0x0D, 0xFF, 0xD9, 0x83, 0x0E, 0xFF, 0xDA, 0x84, 0x0E, 0xFF, 0xD9, + 0x83, 0x0E, 0xFF, 0xD9, 0x82, 0x0E, 0xFF, 0xD8, 0x80, 0x0D, 0xFF, 0xD8, 0x7F, 0x0D, 0xFF, 0xD8, 0x7F, 0x0D, 0xFF, + 0xD8, 0x7F, 0x0D, 0xFF, 0xD8, 0x7F, 0x0D, 0xFF, 0xE4, 0xA6, 0x29, 0xFF, 0xE3, 0xA7, 0x2D, 0xFF, 0xE3, 0xA8, 0x30, + 0xFF, 0xE3, 0xA6, 0x2C, 0xFF, 0xE3, 0xA3, 0x27, 0xFF, 0xE3, 0xA3, 0x27, 0xFF, 0xE3, 0xA3, 0x26, 0xFF, 0xE4, 0xA2, + 0x26, 0xFF, 0xE4, 0xA2, 0x26, 0xFF, 0xE4, 0xA1, 0x26, 0xFF, 0xE4, 0xA1, 0x26, 0xFF, 0xE5, 0xA0, 0x26, 0xFF, 0xE5, + 0x9F, 0x26, 0xFF, 0xE4, 0xA0, 0x25, 0xFF, 0xE4, 0xA0, 0x24, 0xFF, 0xE3, 0x9F, 0x24, 0xFF, 0xE3, 0x9E, 0x24, 0xFF, + 0xE4, 0x9E, 0x23, 0xFF, 0xE6, 0x9F, 0x21, 0xFF, 0xE5, 0x9F, 0x21, 0xFF, 0xE3, 0x9E, 0x22, 0xFF, 0xE5, 0xA4, 0x13, + 0xFF, 0xE7, 0x9F, 0x1A, 0xFF, 0xE7, 0x9F, 0x15, 0xFF, 0xE7, 0xA0, 0x10, 0xFF, 0xEF, 0x9F, 0x11, 0xFF, 0xF7, 0x9E, + 0x12, 0xFF, 0xEC, 0x99, 0x1A, 0xFF, 0xE1, 0x9A, 0x17, 0xFF, 0xE3, 0x9C, 0x14, 0xFF, 0xE5, 0x98, 0x1C, 0xFF, 0xE6, + 0x97, 0x1C, 0xFF, 0xE6, 0x96, 0x1B, 0xFF, 0xDB, 0x98, 0x1B, 0xFF, 0xDF, 0x96, 0x1C, 0xFF, 0xE0, 0x95, 0x1C, 0xFF, + 0xE1, 0x94, 0x1B, 0xFF, 0xE1, 0x93, 0x1B, 0xFF, 0xE0, 0x93, 0x1A, 0xFF, 0xE0, 0x92, 0x1A, 0xFF, 0xE0, 0x92, 0x19, + 0xFF, 0xDF, 0x91, 0x18, 0xFF, 0xDF, 0x90, 0x18, 0xFF, 0xDF, 0x8F, 0x17, 0xFF, 0xDF, 0x8F, 0x16, 0xFF, 0xDE, 0x8E, + 0x15, 0xFF, 0xDD, 0x8D, 0x14, 0xFF, 0xDD, 0x8C, 0x13, 0xFF, 0xDC, 0x8B, 0x12, 0xFF, 0xDC, 0x8A, 0x12, 0xFF, 0xDD, + 0x89, 0x11, 0xFF, 0xDD, 0x87, 0x11, 0xFF, 0xDE, 0x86, 0x10, 0xFF, 0xDC, 0x85, 0x0F, 0xFF, 0xD9, 0x83, 0x0D, 0xFF, + 0xDA, 0x84, 0x0E, 0xFF, 0xDB, 0x85, 0x0F, 0xFF, 0xDA, 0x84, 0x0F, 0xFF, 0xDA, 0x83, 0x0E, 0xFF, 0xDA, 0x81, 0x0E, + 0xFF, 0xD9, 0x80, 0x0D, 0xFF, 0xD9, 0x80, 0x0D, 0xFF, 0xD9, 0x80, 0x0D, 0xFF, 0xD9, 0x80, 0x0D, 0xFF, 0xE7, 0xAA, + 0x2C, 0xFF, 0xE4, 0xAA, 0x30, 0xFF, 0xE2, 0xAA, 0x33, 0xFF, 0xE3, 0xA8, 0x2E, 0xFF, 0xE4, 0xA5, 0x28, 0xFF, 0xE5, + 0xA5, 0x28, 0xFF, 0xE5, 0xA5, 0x28, 0xFF, 0xE5, 0xA4, 0x28, 0xFF, 0xE5, 0xA4, 0x27, 0xFF, 0xE6, 0xA3, 0x27, 0xFF, + 0xE6, 0xA2, 0x27, 0xFF, 0xE7, 0xA1, 0x27, 0xFF, 0xE7, 0xA1, 0x27, 0xFF, 0xE8, 0xA2, 0x25, 0xFF, 0xE9, 0xA3, 0x22, + 0xFF, 0xE7, 0xA0, 0x24, 0xFF, 0xE6, 0x9E, 0x27, 0xFF, 0xE7, 0x9F, 0x25, 0xFF, 0xE8, 0xA0, 0x22, 0xFF, 0xF4, 0xA3, + 0x18, 0xFF, 0xFF, 0xA7, 0x0D, 0xFF, 0xDD, 0xA5, 0x1A, 0xFF, 0xBA, 0x8D, 0x54, 0xFF, 0x9C, 0x83, 0x6E, 0xFF, 0x7D, + 0x79, 0x88, 0xFF, 0x7B, 0x79, 0x8C, 0xFF, 0x79, 0x79, 0x91, 0xFF, 0x94, 0x7A, 0x7E, 0xFF, 0xAF, 0x87, 0x55, 0xFF, + 0xD6, 0x9B, 0x21, 0xFF, 0xFD, 0xA3, 0x04, 0xFF, 0xF4, 0x9D, 0x0F, 0xFF, 0xEB, 0x96, 0x1B, 0xFF, 0xD9, 0x9A, 0x1B, + 0xFF, 0xE4, 0x98, 0x1B, 0xFF, 0xE3, 0x96, 0x1C, 0xFF, 0xE2, 0x95, 0x1C, 0xFF, 0xE2, 0x94, 0x1C, 0xFF, 0xE1, 0x94, + 0x1B, 0xFF, 0xE1, 0x94, 0x1B, 0xFF, 0xE0, 0x93, 0x1B, 0xFF, 0xE0, 0x92, 0x1A, 0xFF, 0xE0, 0x91, 0x19, 0xFF, 0xE1, + 0x90, 0x18, 0xFF, 0xE1, 0x8F, 0x18, 0xFF, 0xE0, 0x8F, 0x16, 0xFF, 0xDF, 0x8E, 0x15, 0xFF, 0xDE, 0x8D, 0x14, 0xFF, + 0xDC, 0x8C, 0x12, 0xFF, 0xDD, 0x8B, 0x12, 0xFF, 0xDE, 0x8A, 0x12, 0xFF, 0xDF, 0x88, 0x11, 0xFF, 0xE0, 0x87, 0x11, + 0xFF, 0xDD, 0x85, 0x0F, 0xFF, 0xDA, 0x83, 0x0D, 0xFF, 0xDB, 0x85, 0x0E, 0xFF, 0xDC, 0x87, 0x10, 0xFF, 0xDC, 0x85, + 0x0F, 0xFF, 0xDB, 0x84, 0x0F, 0xFF, 0xDB, 0x82, 0x0E, 0xFF, 0xDA, 0x81, 0x0D, 0xFF, 0xDA, 0x81, 0x0D, 0xFF, 0xDA, + 0x81, 0x0D, 0xFF, 0xDA, 0x81, 0x0D, 0xFF, 0xE4, 0xAA, 0x30, 0xFF, 0xE8, 0xAF, 0x35, 0xFF, 0xE3, 0xAB, 0x33, 0xFF, + 0xE5, 0xA9, 0x2F, 0xFF, 0xE6, 0xA8, 0x2A, 0xFF, 0xE8, 0xAD, 0x35, 0xFF, 0xE7, 0xA6, 0x25, 0xFF, 0xE7, 0xA7, 0x28, + 0xFF, 0xE7, 0xA8, 0x2B, 0xFF, 0xE5, 0xA6, 0x2D, 0xFF, 0xE4, 0xA4, 0x2E, 0xFF, 0xE6, 0xA4, 0x2B, 0xFF, 0xE8, 0xA4, + 0x29, 0xFF, 0xE5, 0xA4, 0x2A, 0xFF, 0xE1, 0xA5, 0x2C, 0xFF, 0xEF, 0xA9, 0x10, 0xFF, 0xF6, 0xAD, 0x12, 0xFF, 0xF8, + 0xA2, 0x22, 0xFF, 0xA5, 0x91, 0x60, 0xFF, 0x5C, 0x75, 0xA5, 0xFF, 0x14, 0x59, 0xEB, 0xFF, 0x0C, 0x48, 0xFF, 0xFF, + 0x03, 0x55, 0xFA, 0xFF, 0x0F, 0x59, 0xFF, 0xFF, 0x1A, 0x5D, 0xFF, 0xFF, 0x16, 0x60, 0xFF, 0xFF, 0x11, 0x64, 0xF9, + 0xFF, 0x0F, 0x54, 0xFF, 0xFF, 0x0C, 0x4A, 0xFF, 0xFF, 0x17, 0x49, 0xFA, 0xFF, 0x23, 0x47, 0xF5, 0xFF, 0x7E, 0x72, + 0x8D, 0xFF, 0xD9, 0x9D, 0x26, 0xFF, 0xFF, 0xA1, 0x05, 0xFF, 0xE1, 0x96, 0x1D, 0xFF, 0xE9, 0x98, 0x17, 0xFF, 0xE3, + 0x97, 0x1C, 0xFF, 0xE3, 0x97, 0x1A, 0xFF, 0xE4, 0x97, 0x18, 0xFF, 0xE3, 0x96, 0x19, 0xFF, 0xE2, 0x94, 0x1B, 0xFF, + 0xE1, 0x93, 0x1A, 0xFF, 0xE0, 0x93, 0x19, 0xFF, 0xE1, 0x92, 0x18, 0xFF, 0xE1, 0x91, 0x17, 0xFF, 0xE0, 0x90, 0x16, + 0xFF, 0xDF, 0x8F, 0x15, 0xFF, 0xDE, 0x8E, 0x14, 0xFF, 0xDD, 0x8D, 0x13, 0xFF, 0xDE, 0x8D, 0x13, 0xFF, 0xDF, 0x8C, + 0x13, 0xFF, 0xDF, 0x8A, 0x12, 0xFF, 0xE0, 0x89, 0x10, 0xFF, 0xDD, 0x87, 0x0F, 0xFF, 0xDB, 0x84, 0x0E, 0xFF, 0xDF, + 0x8A, 0x13, 0xFF, 0xDB, 0x87, 0x0F, 0xFF, 0xDC, 0x86, 0x0F, 0xFF, 0xDC, 0x85, 0x0F, 0xFF, 0xDB, 0x84, 0x0E, 0xFF, + 0xDB, 0x82, 0x0D, 0xFF, 0xDB, 0x82, 0x0D, 0xFF, 0xDB, 0x82, 0x0D, 0xFF, 0xDB, 0x82, 0x0D, 0xFF, 0xE2, 0xAB, 0x33, + 0xFF, 0xEB, 0xB3, 0x3B, 0xFF, 0xE5, 0xAC, 0x33, 0xFF, 0xE6, 0xAB, 0x30, 0xFF, 0xE7, 0xAA, 0x2D, 0xFF, 0xEA, 0xB6, + 0x43, 0xFF, 0xEA, 0xA7, 0x23, 0xFF, 0xE9, 0xA9, 0x29, 0xFF, 0xE9, 0xAB, 0x2F, 0xFF, 0xE5, 0xA9, 0x32, 0xFF, 0xE2, + 0xA7, 0x35, 0xFF, 0xE6, 0xA7, 0x30, 0xFF, 0xEA, 0xA8, 0x2A, 0xFF, 0xF0, 0xAA, 0x25, 0xFF, 0xF6, 0xAD, 0x1F, 0xFF, + 0xA7, 0x8A, 0x4D, 0xFF, 0x4C, 0x66, 0xB7, 0xFF, 0x0F, 0x54, 0xFF, 0xFF, 0x0C, 0x64, 0xF7, 0xFF, 0x13, 0x63, 0xF8, + 0xFF, 0x1A, 0x61, 0xF9, 0xFF, 0x1E, 0x67, 0xEF, 0xFF, 0x22, 0x61, 0xFC, 0xFF, 0x25, 0x68, 0xFA, 0xFF, 0x28, 0x6F, + 0xF9, 0xFF, 0x22, 0x70, 0xF5, 0xFF, 0x1B, 0x72, 0xF2, 0xFF, 0x1F, 0x6B, 0xF2, 0xFF, 0x24, 0x64, 0xF1, 0xFF, 0x21, + 0x55, 0xFF, 0xFF, 0x1E, 0x53, 0xFF, 0xFF, 0x16, 0x4B, 0xFF, 0xFF, 0x0E, 0x43, 0xFF, 0xFF, 0x5A, 0x61, 0xB1, 0xFF, + 0xDF, 0x95, 0x1E, 0xFF, 0xF0, 0x9A, 0x12, 0xFF, 0xE5, 0x9A, 0x1B, 0xFF, 0xE5, 0x9A, 0x18, 0xFF, 0xE6, 0x9A, 0x14, + 0xFF, 0xE5, 0x98, 0x17, 0xFF, 0xE4, 0x95, 0x1B, 0xFF, 0xE2, 0x95, 0x1A, 0xFF, 0xE0, 0x94, 0x19, 0xFF, 0xE1, 0x93, + 0x18, 0xFF, 0xE2, 0x92, 0x17, 0xFF, 0xE1, 0x91, 0x16, 0xFF, 0xE0, 0x90, 0x16, 0xFF, 0xDF, 0x8F, 0x15, 0xFF, 0xDE, + 0x8F, 0x14, 0xFF, 0xDF, 0x8E, 0x14, 0xFF, 0xE1, 0x8E, 0x14, 0xFF, 0xE0, 0x8C, 0x12, 0xFF, 0xE0, 0x8A, 0x10, 0xFF, + 0xDE, 0x88, 0x10, 0xFF, 0xDC, 0x86, 0x10, 0xFF, 0xE3, 0x8E, 0x17, 0xFF, 0xDB, 0x87, 0x0D, 0xFF, 0xDB, 0x86, 0x0E, + 0xFF, 0xDC, 0x86, 0x0F, 0xFF, 0xDC, 0x85, 0x0E, 0xFF, 0xDB, 0x83, 0x0E, 0xFF, 0xDB, 0x83, 0x0E, 0xFF, 0xDB, 0x83, + 0x0E, 0xFF, 0xDB, 0x83, 0x0E, 0xFF, 0xEA, 0xB0, 0x36, 0xFF, 0xEF, 0xB3, 0x36, 0xFF, 0xED, 0xAE, 0x2E, 0xFF, 0xEC, + 0xAD, 0x2C, 0xFF, 0xEB, 0xAD, 0x2A, 0xFF, 0xEF, 0xB3, 0x40, 0xFF, 0xE9, 0xAA, 0x28, 0xFF, 0xE7, 0xAB, 0x2B, 0xFF, + 0xE6, 0xAB, 0x2F, 0xFF, 0xE6, 0xAA, 0x30, 0xFF, 0xE5, 0xAA, 0x31, 0xFF, 0xE6, 0xA9, 0x2E, 0xFF, 0xE7, 0xA9, 0x2B, + 0xFF, 0xEB, 0xA7, 0x24, 0xFF, 0x5F, 0x6A, 0x93, 0xFF, 0x05, 0x3D, 0xFF, 0xFF, 0x17, 0x56, 0xF9, 0xFF, 0x12, 0x72, + 0xE2, 0xFF, 0x29, 0x72, 0xF8, 0xFF, 0x27, 0x74, 0xF7, 0xFF, 0x25, 0x76, 0xF6, 0xFF, 0x28, 0x76, 0xF1, 0xFF, 0x2A, + 0x70, 0xF8, 0xFF, 0x2D, 0x77, 0xF8, 0xFF, 0x30, 0x7D, 0xF9, 0xFF, 0x2D, 0x7F, 0xF7, 0xFF, 0x2A, 0x81, 0xF5, 0xFF, + 0x2B, 0x7B, 0xF5, 0xFF, 0x2C, 0x75, 0xF5, 0xFF, 0x2B, 0x6A, 0xFD, 0xFF, 0x2A, 0x64, 0xFA, 0xFF, 0x2C, 0x5D, 0xF5, + 0xFF, 0x2E, 0x57, 0xF0, 0xFF, 0x10, 0x48, 0xFF, 0xFF, 0x0E, 0x45, 0xFF, 0xFF, 0x7F, 0x76, 0x80, 0xFF, 0xF0, 0xA7, + 0x02, 0xFF, 0xEA, 0x95, 0x24, 0xFF, 0xE3, 0x9A, 0x19, 0xFF, 0xE4, 0x98, 0x1B, 0xFF, 0xE4, 0x95, 0x1D, 0xFF, 0xE2, + 0x95, 0x1B, 0xFF, 0xDF, 0x96, 0x19, 0xFF, 0xE1, 0x94, 0x18, 0xFF, 0xE2, 0x93, 0x17, 0xFF, 0xE2, 0x92, 0x16, 0xFF, + 0xE1, 0x92, 0x16, 0xFF, 0xE0, 0x91, 0x15, 0xFF, 0xDF, 0x90, 0x15, 0xFF, 0xE0, 0x90, 0x15, 0xFF, 0xE2, 0x91, 0x15, + 0xFF, 0xE1, 0x8E, 0x12, 0xFF, 0xDF, 0x8C, 0x0F, 0xFF, 0xDF, 0x8B, 0x12, 0xFF, 0xDF, 0x8A, 0x14, 0xFF, 0xE2, 0x8D, + 0x15, 0xFF, 0xDC, 0x89, 0x0E, 0xFF, 0xDC, 0x88, 0x0E, 0xFF, 0xDD, 0x87, 0x0F, 0xFF, 0xDC, 0x86, 0x0E, 0xFF, 0xDC, + 0x85, 0x0E, 0xFF, 0xDC, 0x85, 0x0E, 0xFF, 0xDC, 0x85, 0x0E, 0xFF, 0xDC, 0x85, 0x0E, 0xFF, 0xE6, 0xC0, 0x5F, 0xFF, + 0xE8, 0xBE, 0x57, 0xFF, 0xE9, 0xBB, 0x4F, 0xFF, 0xE6, 0xBA, 0x4E, 0xFF, 0xE3, 0xB9, 0x4D, 0xFF, 0xED, 0xB6, 0x50, + 0xFF, 0xE7, 0xAE, 0x2D, 0xFF, 0xE6, 0xAC, 0x2E, 0xFF, 0xE4, 0xAB, 0x2E, 0xFF, 0xE6, 0xAC, 0x2E, 0xFF, 0xE8, 0xAD, + 0x2E, 0xFF, 0xE7, 0xAB, 0x2D, 0xFF, 0xE5, 0xAA, 0x2C, 0xFF, 0xFF, 0xB2, 0x15, 0xFF, 0x10, 0x42, 0xEB, 0xFF, 0x16, + 0x4F, 0xF1, 0xFF, 0x1C, 0x5C, 0xF7, 0xFF, 0x23, 0x71, 0xF8, 0xFF, 0x29, 0x85, 0xF9, 0xFF, 0x2D, 0x88, 0xF6, 0xFF, + 0x30, 0x8B, 0xF3, 0xFF, 0x31, 0x85, 0xF4, 0xFF, 0x33, 0x7F, 0xF4, 0xFF, 0x35, 0x85, 0xF6, 0xFF, 0x37, 0x8B, 0xF9, + 0xFF, 0x38, 0x8D, 0xF8, 0xFF, 0x3A, 0x90, 0xF7, 0xFF, 0x37, 0x8B, 0xF8, 0xFF, 0x35, 0x86, 0xF8, 0xFF, 0x35, 0x7E, + 0xF7, 0xFF, 0x35, 0x75, 0xF6, 0xFF, 0x33, 0x6D, 0xF7, 0xFF, 0x31, 0x64, 0xF7, 0xFF, 0x31, 0x5E, 0xF8, 0xFF, 0x30, + 0x57, 0xF8, 0xFF, 0x25, 0x51, 0xFF, 0xFF, 0x36, 0x51, 0xF5, 0xFF, 0xFD, 0xA4, 0x03, 0xFF, 0xE1, 0x9A, 0x1E, 0xFF, + 0xE3, 0x98, 0x1E, 0xFF, 0xE5, 0x96, 0x1E, 0xFF, 0xE2, 0x96, 0x1C, 0xFF, 0xDF, 0x97, 0x19, 0xFF, 0xE1, 0x96, 0x18, + 0xFF, 0xE3, 0x95, 0x17, 0xFF, 0xE2, 0x94, 0x16, 0xFF, 0xE1, 0x93, 0x16, 0xFF, 0xE0, 0x92, 0x16, 0xFF, 0xE0, 0x91, + 0x15, 0xFF, 0xE2, 0x92, 0x16, 0xFF, 0xE4, 0x93, 0x16, 0xFF, 0xE1, 0x90, 0x12, 0xFF, 0xDF, 0x8E, 0x0F, 0xFF, 0xE1, + 0x8D, 0x14, 0xFF, 0xE3, 0x8D, 0x18, 0xFF, 0xE0, 0x8C, 0x13, 0xFF, 0xDE, 0x8B, 0x0F, 0xFF, 0xDD, 0x89, 0x0F, 0xFF, + 0xDD, 0x88, 0x0E, 0xFF, 0xDD, 0x87, 0x0E, 0xFF, 0xDC, 0x86, 0x0E, 0xFF, 0xDC, 0x86, 0x0E, 0xFF, 0xDC, 0x86, 0x0E, + 0xFF, 0xDC, 0x86, 0x0E, 0xFF, 0xED, 0xB6, 0x3C, 0xFF, 0xEE, 0xB3, 0x35, 0xFF, 0xEF, 0xB1, 0x2F, 0xFF, 0xED, 0xB1, + 0x2F, 0xFF, 0xEC, 0xB0, 0x2F, 0xFF, 0xEE, 0xB0, 0x38, 0xFF, 0xE9, 0xAE, 0x2D, 0xFF, 0xE7, 0xAD, 0x2F, 0xFF, 0xE6, + 0xAD, 0x30, 0xFF, 0xE8, 0xAE, 0x2F, 0xFF, 0xEA, 0xB0, 0x2D, 0xFF, 0xEC, 0xAD, 0x30, 0xFF, 0xEE, 0xAF, 0x28, 0xFF, + 0xC8, 0xA9, 0x2F, 0xFF, 0x04, 0x3D, 0xFF, 0xFF, 0x19, 0x50, 0xFA, 0xFF, 0x21, 0x5F, 0xF8, 0xFF, 0x28, 0x73, 0xF7, + 0xFF, 0x2F, 0x87, 0xF7, 0xFF, 0x37, 0x95, 0xFA, 0xFF, 0x37, 0x9B, 0xF5, 0xFF, 0x3A, 0x96, 0xF5, 0xFF, 0x3D, 0x92, + 0xF5, 0xFF, 0x3F, 0x94, 0xF7, 0xFF, 0x41, 0x96, 0xF9, 0xFF, 0x43, 0x99, 0xF9, 0xFF, 0x46, 0x9D, 0xF9, 0xFF, 0x44, + 0x98, 0xF8, 0xFF, 0x43, 0x94, 0xF7, 0xFF, 0x42, 0x8D, 0xF8, 0xFF, 0x41, 0x86, 0xF9, 0xFF, 0x3F, 0x7D, 0xF9, 0xFF, + 0x3C, 0x73, 0xF9, 0xFF, 0x38, 0x70, 0xF7, 0xFF, 0x35, 0x6C, 0xF4, 0xFF, 0x21, 0x60, 0xFF, 0xFF, 0x62, 0x6C, 0xBE, + 0xFF, 0xEF, 0x9D, 0x12, 0xFF, 0xE8, 0x9A, 0x21, 0xFF, 0xED, 0x99, 0x1C, 0xFF, 0xE3, 0x9B, 0x17, 0xFF, 0xF0, 0x98, + 0x13, 0xFF, 0xE0, 0x94, 0x1B, 0xFF, 0xE1, 0x96, 0x1A, 0xFF, 0xE3, 0x97, 0x19, 0xFF, 0xE4, 0x96, 0x18, 0xFF, 0xE5, + 0x95, 0x17, 0xFF, 0xE3, 0x94, 0x18, 0xFF, 0xE2, 0x93, 0x19, 0xFF, 0xE0, 0x91, 0x16, 0xFF, 0xDE, 0x90, 0x14, 0xFF, + 0xE1, 0x91, 0x15, 0xFF, 0xE5, 0x92, 0x16, 0xFF, 0xE3, 0x90, 0x14, 0xFF, 0xE2, 0x8D, 0x11, 0xFF, 0xE2, 0x8D, 0x10, + 0xFF, 0xE3, 0x8D, 0x0F, 0xFF, 0xDE, 0x8A, 0x10, 0xFF, 0xD8, 0x88, 0x11, 0xFF, 0xE1, 0x87, 0x0E, 0xFF, 0xDC, 0x89, + 0x0B, 0xFF, 0xE0, 0x85, 0x10, 0xFF, 0xE4, 0x87, 0x09, 0xFF, 0xE4, 0x87, 0x09, 0xFF, 0xE8, 0xB5, 0x3F, 0xFF, 0xE9, + 0xB3, 0x3B, 0xFF, 0xEA, 0xB2, 0x36, 0xFF, 0xE9, 0xB1, 0x37, 0xFF, 0xE8, 0xB1, 0x37, 0xFF, 0xE9, 0xAF, 0x32, 0xFF, + 0xEA, 0xAE, 0x2D, 0xFF, 0xE9, 0xAE, 0x30, 0xFF, 0xE8, 0xAF, 0x32, 0xFF, 0xEA, 0xB1, 0x30, 0xFF, 0xEC, 0xB4, 0x2D, + 0xFF, 0xF1, 0xAE, 0x34, 0xFF, 0xF6, 0xB4, 0x24, 0xFF, 0x86, 0x7E, 0x8D, 0xFF, 0x00, 0x4E, 0xF6, 0xFF, 0x1D, 0x5C, + 0xEC, 0xFF, 0x25, 0x63, 0xF9, 0xFF, 0x2D, 0x76, 0xF7, 0xFF, 0x35, 0x89, 0xF4, 0xFF, 0x41, 0xA2, 0xFD, 0xFF, 0x3E, + 0xAB, 0xF6, 0xFF, 0x43, 0xA8, 0xF6, 0xFF, 0x47, 0xA4, 0xF7, 0xFF, 0x4A, 0xA3, 0xF8, 0xFF, 0x4C, 0xA1, 0xFA, 0xFF, + 0x4E, 0xA5, 0xFA, 0xFF, 0x51, 0xAA, 0xFB, 0xFF, 0x52, 0xA6, 0xF9, 0xFF, 0x52, 0xA2, 0xF7, 0xFF, 0x4F, 0x9C, 0xFA, + 0xFF, 0x4D, 0x97, 0xFD, 0xFF, 0x4A, 0x8D, 0xFC, 0xFF, 0x47, 0x83, 0xFB, 0xFF, 0x40, 0x82, 0xF6, 0xFF, 0x39, 0x82, + 0xF1, 0xFF, 0x2B, 0x72, 0xF4, 0xFF, 0xAB, 0x8C, 0x71, 0xFF, 0xF0, 0x99, 0x16, 0xFF, 0xEF, 0x99, 0x25, 0xFF, 0xE8, + 0x97, 0x25, 0xFF, 0xC5, 0x9A, 0x26, 0xFF, 0xF0, 0x96, 0x16, 0xFF, 0xE2, 0x91, 0x1C, 0xFF, 0xE2, 0x96, 0x1B, 0xFF, + 0xE2, 0x9A, 0x1B, 0xFF, 0xE5, 0x99, 0x19, 0xFF, 0xE8, 0x98, 0x18, 0xFF, 0xE6, 0x96, 0x1A, 0xFF, 0xE4, 0x95, 0x1C, + 0xFF, 0xDF, 0x91, 0x17, 0xFF, 0xD9, 0x8D, 0x13, 0xFF, 0xE2, 0x92, 0x18, 0xFF, 0xEA, 0x97, 0x1E, 0xFF, 0xE5, 0x92, + 0x14, 0xFF, 0xE1, 0x8D, 0x0B, 0xFF, 0xE5, 0x8E, 0x0D, 0xFF, 0xE9, 0x8F, 0x10, 0xFF, 0xDE, 0x8B, 0x12, 0xFF, 0xD4, + 0x88, 0x14, 0xFF, 0xE6, 0x87, 0x0E, 0xFF, 0xDC, 0x8C, 0x08, 0xFF, 0xE4, 0x84, 0x11, 0xFF, 0xEC, 0x88, 0x03, 0xFF, + 0xEC, 0x88, 0x03, 0xFF, 0xEA, 0xB6, 0x3D, 0xFF, 0xEA, 0xB5, 0x3A, 0xFF, 0xEB, 0xB4, 0x38, 0xFF, 0xEB, 0xB3, 0x37, + 0xFF, 0xEA, 0xB3, 0x37, 0xFF, 0xEB, 0xB2, 0x34, 0xFF, 0xEB, 0xB1, 0x32, 0xFF, 0xEB, 0xB1, 0x33, 0xFF, 0xEA, 0xB0, + 0x34, 0xFF, 0xE9, 0xB3, 0x32, 0xFF, 0xE8, 0xB5, 0x2F, 0xFF, 0xF0, 0xB0, 0x34, 0xFF, 0xF8, 0xB6, 0x22, 0xFF, 0x44, + 0x60, 0xC5, 0xFF, 0x0B, 0x53, 0xF9, 0xFF, 0x21, 0x63, 0xF2, 0xFF, 0x29, 0x6F, 0xF6, 0xFF, 0x2F, 0x7D, 0xF6, 0xFF, + 0x35, 0x8A, 0xF7, 0xFF, 0x41, 0xA1, 0xFA, 0xFF, 0x45, 0xAF, 0xF6, 0xFF, 0x4F, 0xB4, 0xFA, 0xFF, 0x50, 0xB0, 0xF6, + 0xFF, 0x53, 0xAE, 0xF8, 0xFF, 0x56, 0xAC, 0xFA, 0xFF, 0x59, 0xB2, 0xFC, 0xFF, 0x5D, 0xB7, 0xFD, 0xFF, 0x5F, 0xB3, + 0xFA, 0xFF, 0x61, 0xAF, 0xF6, 0xFF, 0x5D, 0xAC, 0xF9, 0xFF, 0x59, 0xA9, 0xFD, 0xFF, 0x55, 0x9F, 0xFB, 0xFF, 0x50, + 0x94, 0xF8, 0xFF, 0x4A, 0x91, 0xF7, 0xFF, 0x44, 0x8D, 0xF5, 0xFF, 0x22, 0x7D, 0xFF, 0xFF, 0xEF, 0xA5, 0x1A, 0xFF, + 0xF3, 0x9E, 0x12, 0xFF, 0xF1, 0x96, 0x28, 0xFF, 0xB0, 0x9F, 0x22, 0xFF, 0x00, 0x96, 0x6C, 0xFF, 0x82, 0x9B, 0x3B, + 0xFF, 0xF8, 0x9D, 0x16, 0xFF, 0xF4, 0x9B, 0x15, 0xFF, 0xE2, 0x9C, 0x14, 0xFF, 0xE4, 0x99, 0x15, 0xFF, 0xE6, 0x96, + 0x17, 0xFF, 0xE5, 0x95, 0x18, 0xFF, 0xE4, 0x93, 0x1A, 0xFF, 0xE2, 0x93, 0x18, 0xFF, 0xE0, 0x92, 0x16, 0xFF, 0xE6, + 0x98, 0x1C, 0xFF, 0xE4, 0x95, 0x19, 0xFF, 0xE4, 0x92, 0x16, 0xFF, 0xE5, 0x8F, 0x12, 0xFF, 0xEB, 0x8C, 0x12, 0xFF, + 0xE3, 0x8B, 0x12, 0xFF, 0xE3, 0x87, 0x00, 0xFF, 0xF4, 0x7B, 0x00, 0xFF, 0xD3, 0x86, 0x1A, 0xFF, 0xF0, 0x8C, 0x0C, + 0xFF, 0xE2, 0x8E, 0x00, 0xFF, 0xEA, 0x84, 0x0D, 0xFF, 0xF1, 0x86, 0x07, 0xFF, 0xEC, 0xB7, 0x3B, 0xFF, 0xEC, 0xB6, + 0x3A, 0xFF, 0xEC, 0xB6, 0x39, 0xFF, 0xEC, 0xB5, 0x38, 0xFF, 0xED, 0xB5, 0x37, 0xFF, 0xEC, 0xB4, 0x37, 0xFF, 0xEC, + 0xB4, 0x37, 0xFF, 0xEC, 0xB3, 0x36, 0xFF, 0xEC, 0xB2, 0x36, 0xFF, 0xE8, 0xB4, 0x33, 0xFF, 0xE4, 0xB5, 0x31, 0xFF, + 0xEF, 0xB1, 0x34, 0xFF, 0xF9, 0xB8, 0x21, 0xFF, 0x02, 0x41, 0xFD, 0xFF, 0x1E, 0x58, 0xFC, 0xFF, 0x25, 0x6A, 0xF8, + 0xFF, 0x2C, 0x7C, 0xF3, 0xFF, 0x31, 0x84, 0xF6, 0xFF, 0x35, 0x8B, 0xF9, 0xFF, 0x41, 0xA0, 0xF7, 0xFF, 0x4C, 0xB4, + 0xF6, 0xFF, 0x5B, 0xC0, 0xFE, 0xFF, 0x59, 0xBC, 0xF6, 0xFF, 0x5D, 0xBA, 0xF8, 0xFF, 0x60, 0xB7, 0xFA, 0xFF, 0x64, + 0xBE, 0xFD, 0xFF, 0x69, 0xC4, 0xFF, 0xFF, 0x6C, 0xC0, 0xFA, 0xFF, 0x6F, 0xBD, 0xF5, 0xFF, 0x6A, 0xBC, 0xF9, 0xFF, + 0x65, 0xBB, 0xFD, 0xFF, 0x60, 0xB1, 0xFA, 0xFF, 0x5A, 0xA6, 0xF6, 0xFF, 0x54, 0x9F, 0xF8, 0xFF, 0x4F, 0x98, 0xFA, + 0xFF, 0x6E, 0x94, 0xDF, 0xFF, 0xFB, 0xA6, 0x07, 0xFF, 0xDA, 0x9C, 0x24, 0xFF, 0xF2, 0x9F, 0x14, 0xFF, 0x71, 0xA1, + 0x4A, 0xFF, 0x0D, 0xA9, 0x68, 0xFF, 0x06, 0xA3, 0x61, 0xFF, 0x1B, 0x98, 0x5A, 0xFF, 0x9B, 0x96, 0x33, 0xFF, 0xFE, + 0x99, 0x0D, 0xFF, 0xF1, 0x96, 0x11, 0xFF, 0xE4, 0x94, 0x16, 0xFF, 0xE4, 0x93, 0x17, 0xFF, 0xE4, 0x91, 0x18, 0xFF, + 0xE5, 0x94, 0x19, 0xFF, 0xE6, 0x98, 0x1A, 0xFF, 0xEA, 0x9D, 0x1F, 0xFF, 0xDE, 0x93, 0x15, 0xFF, 0xE3, 0x92, 0x17, + 0xFF, 0xE8, 0x91, 0x1A, 0xFF, 0xEB, 0x94, 0x1F, 0xFF, 0xD1, 0x9D, 0x25, 0xFF, 0x72, 0xF7, 0xD0, 0xFF, 0x95, 0xF2, + 0xC1, 0xFF, 0xF0, 0x83, 0x00, 0xFF, 0xA0, 0x81, 0x17, 0xFF, 0x2E, 0x7E, 0x3B, 0xFF, 0xCB, 0x87, 0x16, 0xFF, 0xDA, + 0x8A, 0x0B, 0xFF, 0xEC, 0xB8, 0x3D, 0xFF, 0xED, 0xB8, 0x3C, 0xFF, 0xED, 0xB7, 0x3B, 0xFF, 0xED, 0xB7, 0x3A, 0xFF, + 0xED, 0xB6, 0x39, 0xFF, 0xED, 0xB6, 0x39, 0xFF, 0xED, 0xB6, 0x39, 0xFF, 0xED, 0xB6, 0x39, 0xFF, 0xED, 0xB6, 0x39, + 0xFF, 0xEC, 0xB4, 0x37, 0xFF, 0xEB, 0xB2, 0x34, 0xFF, 0xF2, 0xAB, 0x34, 0xFF, 0xB3, 0x95, 0x6D, 0xFF, 0x00, 0x46, + 0xFF, 0xFF, 0x20, 0x64, 0xF7, 0xFF, 0x28, 0x73, 0xF6, 0xFF, 0x30, 0x81, 0xF5, 0xFF, 0x37, 0x8B, 0xF6, 0xFF, 0x3D, + 0x94, 0xF8, 0xFF, 0x48, 0xA6, 0xF8, 0xFF, 0x53, 0xB7, 0xF7, 0xFF, 0x60, 0xC2, 0xFB, 0xFF, 0x65, 0xC4, 0xF7, 0xFF, + 0x69, 0xC3, 0xF9, 0xFF, 0x6D, 0xC2, 0xFA, 0xFF, 0x72, 0xC6, 0xFA, 0xFF, 0x77, 0xCB, 0xFA, 0xFF, 0x7A, 0xCB, 0xFB, + 0xFF, 0x7D, 0xCB, 0xFC, 0xFF, 0x7A, 0xC8, 0xFA, 0xFF, 0x77, 0xC5, 0xF8, 0xFF, 0x72, 0xBC, 0xF9, 0xFF, 0x6C, 0xB4, + 0xFA, 0xFF, 0x68, 0xB0, 0xF6, 0xFF, 0x56, 0xAA, 0xFD, 0xFF, 0xA5, 0xA0, 0x93, 0xFF, 0xF3, 0xA1, 0x13, 0xFF, 0xEF, + 0x9C, 0x21, 0xFF, 0xFF, 0x9D, 0x19, 0xFF, 0x23, 0xC1, 0x71, 0xFF, 0x25, 0xB7, 0x79, 0xFF, 0x1D, 0xB2, 0x71, 0xFF, + 0x23, 0xAA, 0x6A, 0xFF, 0x25, 0xA0, 0x66, 0xFF, 0x18, 0x9A, 0x63, 0xFF, 0x72, 0x9C, 0x41, 0xFF, 0xCB, 0x9F, 0x1E, + 0xFF, 0xFF, 0x93, 0x18, 0xFF, 0xF1, 0x98, 0x13, 0xFF, 0xF4, 0x9C, 0x18, 0xFF, 0xF7, 0xA0, 0x1D, 0xFF, 0xFF, 0x9C, + 0x1B, 0xFF, 0xF6, 0x93, 0x10, 0xFF, 0xF1, 0x93, 0x11, 0xFF, 0xEC, 0x93, 0x13, 0xFF, 0xFF, 0x83, 0x00, 0xFF, 0xA0, + 0xCB, 0x72, 0xFF, 0x81, 0xF9, 0xCB, 0xFF, 0xAC, 0xFF, 0xD0, 0xFF, 0x45, 0xA0, 0x78, 0xFF, 0x00, 0x77, 0x33, 0xFF, + 0x02, 0x7C, 0x3A, 0xFF, 0xE2, 0x8C, 0x0D, 0xFF, 0xDB, 0x8E, 0x0D, 0xFF, 0xED, 0xBA, 0x3E, 0xFF, 0xED, 0xB9, 0x3D, + 0xFF, 0xED, 0xB9, 0x3C, 0xFF, 0xED, 0xB8, 0x3B, 0xFF, 0xED, 0xB8, 0x3A, 0xFF, 0xED, 0xB8, 0x3B, 0xFF, 0xED, 0xB8, + 0x3B, 0xFF, 0xEE, 0xB8, 0x3C, 0xFF, 0xEE, 0xB9, 0x3C, 0xFF, 0xF0, 0xB4, 0x3A, 0xFF, 0xF2, 0xAE, 0x37, 0xFF, 0xFE, + 0xB3, 0x32, 0xFF, 0x7C, 0x8E, 0xB3, 0xFF, 0x06, 0x58, 0xFF, 0xFF, 0x22, 0x71, 0xF3, 0xFF, 0x2B, 0x7C, 0xF4, 0xFF, + 0x34, 0x86, 0xF6, 0xFF, 0x3D, 0x92, 0xF7, 0xFF, 0x45, 0x9D, 0xF8, 0xFF, 0x4F, 0xAC, 0xF8, 0xFF, 0x5A, 0xBB, 0xF8, + 0xFF, 0x65, 0xC4, 0xF9, 0xFF, 0x70, 0xCC, 0xF9, 0xFF, 0x75, 0xCC, 0xFA, 0xFF, 0x7A, 0xCC, 0xFA, 0xFF, 0x80, 0xCF, + 0xF7, 0xFF, 0x85, 0xD2, 0xF4, 0xFF, 0x89, 0xD5, 0xFB, 0xFF, 0x8C, 0xD9, 0xFF, 0xFF, 0x8B, 0xD3, 0xFA, 0xFF, 0x89, + 0xCE, 0xF2, 0xFF, 0x84, 0xC8, 0xF8, 0xFF, 0x7F, 0xC1, 0xFE, 0xFF, 0x7C, 0xC1, 0xF4, 0xFF, 0x5E, 0xBC, 0xFF, 0xFF, + 0xDB, 0xAB, 0x47, 0xFF, 0xEA, 0x9C, 0x1E, 0xFF, 0xE8, 0xA2, 0x1D, 0xFF, 0xE5, 0xA7, 0x1D, 0xFF, 0x1B, 0xD3, 0x98, + 0xFF, 0x21, 0xCB, 0x8A, 0xFF, 0x26, 0xC3, 0x82, 0xFF, 0x2C, 0xBB, 0x7A, 0xFF, 0x28, 0xB4, 0x75, 0xFF, 0x25, 0xAD, + 0x70, 0xFF, 0x16, 0xAB, 0x6D, 0xFF, 0x08, 0xA9, 0x6A, 0xFF, 0x11, 0xA9, 0x5E, 0xFF, 0x53, 0x9E, 0x51, 0xFF, 0x6D, + 0x9B, 0x47, 0xFF, 0x87, 0x97, 0x3E, 0xFF, 0x91, 0x95, 0x3B, 0xFF, 0x80, 0x98, 0x38, 0xFF, 0x63, 0x96, 0x44, 0xFF, + 0x45, 0x94, 0x4F, 0xFF, 0x3C, 0xB4, 0x82, 0xFF, 0x1B, 0x84, 0x4F, 0xFF, 0x87, 0xE0, 0xAF, 0xFF, 0x82, 0xCC, 0x9E, + 0xFF, 0x11, 0x7F, 0x35, 0xFF, 0x1B, 0x82, 0x42, 0xFF, 0x3B, 0x84, 0x32, 0xFF, 0xF9, 0x92, 0x04, 0xFF, 0xDC, 0x92, + 0x0F, 0xFF, 0xEE, 0xBC, 0x40, 0xFF, 0xED, 0xBB, 0x3F, 0xFF, 0xED, 0xBA, 0x3E, 0xFF, 0xED, 0xBA, 0x3D, 0xFF, 0xEC, + 0xB9, 0x3C, 0xFF, 0xEC, 0xB9, 0x3C, 0xFF, 0xEC, 0xB8, 0x3C, 0xFF, 0xEC, 0xB8, 0x3C, 0xFF, 0xEB, 0xB8, 0x3C, 0xFF, + 0xF0, 0xB3, 0x3F, 0xFF, 0xF4, 0xAF, 0x42, 0xFF, 0xE8, 0xBA, 0x0D, 0xFF, 0x96, 0xB8, 0xFF, 0xFF, 0x4C, 0x81, 0xF6, + 0xFF, 0x22, 0x75, 0xF5, 0xFF, 0x2D, 0x80, 0xF6, 0xFF, 0x38, 0x8B, 0xF7, 0xFF, 0x42, 0x99, 0xF7, 0xFF, 0x4D, 0xA6, + 0xF7, 0xFF, 0x56, 0xB2, 0xF8, 0xFF, 0x5F, 0xBD, 0xF9, 0xFF, 0x6D, 0xC8, 0xF9, 0xFF, 0x7A, 0xD4, 0xFA, 0xFF, 0x81, + 0xD5, 0xFA, 0xFF, 0x88, 0xD7, 0xF9, 0xFF, 0x8D, 0xD8, 0xFA, 0xFF, 0x92, 0xDA, 0xFB, 0xFF, 0xA1, 0xE4, 0xF9, 0xFF, + 0x91, 0xD6, 0xFE, 0xFF, 0x9F, 0xDE, 0xFA, 0xFF, 0x97, 0xDB, 0xF8, 0xFF, 0x93, 0xD5, 0xF9, 0xFF, 0x8F, 0xCF, 0xFB, + 0xFF, 0x85, 0xD1, 0xFF, 0xFF, 0x78, 0xC6, 0xFF, 0xFF, 0xFC, 0x9A, 0x00, 0xFF, 0xF1, 0xA8, 0x26, 0xFF, 0xF8, 0xA4, + 0x1F, 0xFF, 0xA5, 0xBD, 0x53, 0xFF, 0x30, 0xDA, 0xA4, 0xFF, 0x37, 0xD5, 0x9D, 0xFF, 0x3A, 0xD0, 0x97, 0xFF, 0x3D, + 0xCA, 0x90, 0xFF, 0x39, 0xC5, 0x8A, 0xFF, 0x35, 0xBF, 0x84, 0xFF, 0x30, 0xBD, 0x7C, 0xFF, 0x2C, 0xBC, 0x74, 0xFF, + 0x1B, 0xB8, 0x75, 0xFF, 0x27, 0xAF, 0x77, 0xFF, 0x25, 0xAB, 0x72, 0xFF, 0x23, 0xA7, 0x6D, 0xFF, 0x28, 0xA3, 0x6A, + 0xFF, 0x1E, 0xA2, 0x68, 0xFF, 0x19, 0x95, 0x57, 0xFF, 0x45, 0xB7, 0x77, 0xFF, 0x81, 0xF0, 0xBA, 0xFF, 0x4C, 0xAC, + 0x72, 0xFF, 0x14, 0x7B, 0x41, 0xFF, 0x1D, 0x8A, 0x4F, 0xFF, 0x1C, 0x86, 0x42, 0xFF, 0x14, 0x86, 0x49, 0xFF, 0x8B, + 0x86, 0x16, 0xFF, 0xF5, 0x90, 0x0A, 0xFF, 0xE7, 0x8D, 0x15, 0xFF, 0xEF, 0xBE, 0x41, 0xFF, 0xEE, 0xBD, 0x40, 0xFF, + 0xED, 0xBC, 0x3F, 0xFF, 0xED, 0xBB, 0x3E, 0xFF, 0xEC, 0xBA, 0x3D, 0xFF, 0xEB, 0xBA, 0x3D, 0xFF, 0xEA, 0xB9, 0x3C, + 0xFF, 0xE9, 0xB8, 0x3C, 0xFF, 0xE8, 0xB7, 0x3B, 0xFF, 0xF0, 0xB9, 0x39, 0xFF, 0xF7, 0xBA, 0x37, 0xFF, 0xDC, 0xB5, + 0x50, 0xFF, 0x44, 0x96, 0xFF, 0xFF, 0x9C, 0xC4, 0xFE, 0xFF, 0x23, 0x79, 0xF7, 0xFF, 0x30, 0x85, 0xF8, 0xFF, 0x3C, + 0x91, 0xF8, 0xFF, 0x48, 0xA0, 0xF8, 0xFF, 0x55, 0xAF, 0xF7, 0xFF, 0x5D, 0xB7, 0xF8, 0xFF, 0x65, 0xBF, 0xF9, 0xFF, + 0x75, 0xCD, 0xFA, 0xFF, 0x85, 0xDB, 0xFB, 0xFF, 0x8D, 0xDE, 0xFA, 0xFF, 0x95, 0xE1, 0xF9, 0xFF, 0x9A, 0xE1, 0xFD, + 0xFF, 0xA0, 0xE2, 0xFF, 0xFF, 0xA3, 0xE8, 0xFA, 0xFF, 0x6B, 0xBD, 0xFF, 0xFF, 0x9E, 0xDE, 0xFC, 0xFF, 0xA6, 0xE8, + 0xFF, 0xFF, 0xA3, 0xE3, 0xFB, 0xFF, 0xA0, 0xDE, 0xF7, 0xFF, 0x99, 0xD7, 0xFD, 0xFF, 0xAB, 0xBD, 0xB5, 0xFF, 0xF0, + 0x9F, 0x11, 0xFF, 0xE8, 0xA3, 0x1D, 0xFF, 0xFF, 0x9E, 0x19, 0xFF, 0x65, 0xD4, 0x89, 0xFF, 0x45, 0xE1, 0xB0, 0xFF, + 0x4D, 0xDF, 0xB0, 0xFF, 0x4D, 0xDC, 0xAB, 0xFF, 0x4D, 0xD8, 0xA7, 0xFF, 0x49, 0xD5, 0xA0, 0xFF, 0x44, 0xD2, 0x99, + 0xFF, 0x3C, 0xCD, 0x97, 0xFF, 0x34, 0xC9, 0x94, 0xFF, 0x34, 0xC4, 0x8D, 0xFF, 0x33, 0xC0, 0x86, 0xFF, 0x32, 0xBC, + 0x7A, 0xFF, 0x31, 0xB7, 0x6E, 0xFF, 0x2F, 0xB2, 0x6D, 0xFF, 0x2E, 0xAE, 0x6B, 0xFF, 0x3F, 0xB9, 0x7D, 0xFF, 0x30, + 0xA5, 0x6F, 0xFF, 0x4E, 0xB5, 0x7B, 0xFF, 0x20, 0x9A, 0x56, 0xFF, 0x2A, 0x9F, 0x5B, 0xFF, 0x24, 0x93, 0x50, 0xFF, + 0x65, 0xB9, 0x80, 0xFF, 0x1C, 0x99, 0x5F, 0xFF, 0xE2, 0x8F, 0x03, 0xFF, 0xF2, 0x8E, 0x10, 0xFF, 0xF2, 0x88, 0x1B, + 0xFF, 0xEF, 0xBF, 0x43, 0xFF, 0xEE, 0xBE, 0x42, 0xFF, 0xEE, 0xBD, 0x41, 0xFF, 0xEE, 0xBD, 0x40, 0xFF, 0xED, 0xBC, + 0x3F, 0xFF, 0xEC, 0xBB, 0x3F, 0xFF, 0xEB, 0xB9, 0x3F, 0xFF, 0xEC, 0xB9, 0x3D, 0xFF, 0xEE, 0xB8, 0x3C, 0xFF, 0xEB, + 0xB8, 0x37, 0xFF, 0xF6, 0xBC, 0x26, 0xFF, 0x8F, 0x9B, 0x94, 0xFF, 0x37, 0x96, 0xFB, 0xFF, 0x7C, 0xBB, 0xF9, 0xFF, + 0x85, 0xB5, 0xF8, 0xFF, 0x49, 0x99, 0xF6, 0xFF, 0x42, 0x9B, 0xF5, 0xFF, 0x4E, 0xA6, 0xF6, 0xFF, 0x59, 0xB2, 0xF7, + 0xFF, 0x65, 0xBC, 0xF8, 0xFF, 0x72, 0xC6, 0xF9, 0xFF, 0x7F, 0xD3, 0xF9, 0xFF, 0x8D, 0xE0, 0xFA, 0xFF, 0x97, 0xE5, + 0xF9, 0xFF, 0xA1, 0xEB, 0xF8, 0xFF, 0xA6, 0xEA, 0xFE, 0xFF, 0xAA, 0xEA, 0xFF, 0xFF, 0xA8, 0xEE, 0xFC, 0xFF, 0x62, + 0xBA, 0xF9, 0xFF, 0x98, 0xDC, 0xFA, 0xFF, 0xB9, 0xF3, 0xFE, 0xFF, 0xB2, 0xEC, 0xFB, 0xFF, 0xAB, 0xE5, 0xF7, 0xFF, + 0xA2, 0xE4, 0xFE, 0xFF, 0xD1, 0xB0, 0x64, 0xFF, 0xF0, 0x9F, 0x19, 0xFF, 0xE8, 0x9E, 0x26, 0xFF, 0xF2, 0x98, 0x03, + 0xFF, 0x50, 0xEF, 0xE3, 0xFF, 0x57, 0xEE, 0xD5, 0xFF, 0x64, 0xE3, 0xBF, 0xFF, 0x64, 0xE1, 0xBC, 0xFF, 0x64, 0xDF, + 0xB9, 0xFF, 0x5D, 0xDD, 0xB4, 0xFF, 0x56, 0xDB, 0xB0, 0xFF, 0x4E, 0xD7, 0xA9, 0xFF, 0x46, 0xD3, 0xA2, 0xFF, 0x42, + 0xD0, 0x9B, 0xFF, 0x3F, 0xCD, 0x93, 0xFF, 0x3D, 0xC9, 0x8B, 0xFF, 0x3C, 0xC5, 0x84, 0xFF, 0x39, 0xC1, 0x80, 0xFF, + 0x36, 0xBC, 0x7D, 0xFF, 0x45, 0xC7, 0x8A, 0xFF, 0x44, 0xC1, 0x88, 0xFF, 0x2B, 0xA0, 0x62, 0xFF, 0x2B, 0xA9, 0x64, + 0xFF, 0x2D, 0xA3, 0x5E, 0xFF, 0x26, 0x95, 0x4F, 0xFF, 0x98, 0xCE, 0xA4, 0xFF, 0xDC, 0xEA, 0xD8, 0xFF, 0xFF, 0xDC, + 0xB9, 0xFF, 0xF3, 0x9D, 0x38, 0xFF, 0xD3, 0x8F, 0x00, 0xFF, 0xEF, 0xC1, 0x45, 0xFF, 0xEF, 0xC0, 0x44, 0xFF, 0xEF, + 0xBF, 0x43, 0xFF, 0xEF, 0xBE, 0x41, 0xFF, 0xEF, 0xBD, 0x40, 0xFF, 0xED, 0xBC, 0x41, 0xFF, 0xEB, 0xBA, 0x42, 0xFF, + 0xEF, 0xBA, 0x3F, 0xFF, 0xF3, 0xB9, 0x3C, 0xFF, 0xE6, 0xB8, 0x34, 0xFF, 0xF6, 0xBD, 0x16, 0xFF, 0x4F, 0x7F, 0xD8, + 0xFF, 0x46, 0x90, 0xF7, 0xFF, 0x54, 0xA5, 0xF7, 0xFF, 0xBA, 0xDA, 0xFF, 0xFF, 0x4D, 0xA1, 0xF8, 0xFF, 0x49, 0xA5, + 0xF3, 0xFF, 0x53, 0xAD, 0xF4, 0xFF, 0x5D, 0xB5, 0xF6, 0xFF, 0x6E, 0xC0, 0xF8, 0xFF, 0x7F, 0xCC, 0xFA, 0xFF, 0x8A, + 0xD8, 0xF9, 0xFF, 0x95, 0xE4, 0xF8, 0xFF, 0xA1, 0xEC, 0xF8, 0xFF, 0xAE, 0xF4, 0xF7, 0xFF, 0xB2, 0xF3, 0xFE, 0xFF, + 0xB5, 0xF1, 0xFF, 0xFF, 0xAD, 0xF4, 0xFE, 0xFF, 0x59, 0xB6, 0xF3, 0xFF, 0x92, 0xDA, 0xF8, 0xFF, 0xCC, 0xFF, 0xFE, + 0xFF, 0xC1, 0xF6, 0xFA, 0xFF, 0xB6, 0xED, 0xF7, 0xFF, 0xAB, 0xF1, 0xFF, 0xFF, 0xF7, 0xA4, 0x13, 0xFF, 0xEF, 0xA4, + 0x15, 0xFF, 0xE8, 0xA5, 0x18, 0xFF, 0xCD, 0xB4, 0x56, 0xFF, 0x71, 0xF2, 0xF0, 0xFF, 0x84, 0xEF, 0xD4, 0xFF, 0x7B, + 0xE6, 0xCF, 0xFF, 0x7B, 0xE6, 0xCD, 0xFF, 0x7C, 0xE6, 0xCB, 0xFF, 0x71, 0xE5, 0xC9, 0xFF, 0x67, 0xE5, 0xC6, 0xFF, + 0x5F, 0xE1, 0xBC, 0xFF, 0x57, 0xDD, 0xB1, 0xFF, 0x51, 0xDB, 0xA8, 0xFF, 0x4B, 0xDA, 0xA0, 0xFF, 0x48, 0xD7, 0x9C, + 0xFF, 0x46, 0xD4, 0x99, 0xFF, 0x42, 0xCF, 0x94, 0xFF, 0x3E, 0xCA, 0x8F, 0xFF, 0x3B, 0xC4, 0x88, 0xFF, 0x39, 0xBE, + 0x81, 0xFF, 0x30, 0xB3, 0x72, 0xFF, 0x27, 0xA8, 0x62, 0xFF, 0x27, 0xA0, 0x58, 0xFF, 0x27, 0x97, 0x4E, 0xFF, 0x79, + 0xC4, 0x9F, 0xFF, 0xF7, 0xFB, 0xFF, 0xFF, 0xF4, 0xD2, 0x7F, 0xFF, 0xE1, 0x8E, 0x03, 0xFF, 0xE1, 0x89, 0x0E, 0xFF, + 0xEF, 0xC3, 0x47, 0xFF, 0xEF, 0xC2, 0x46, 0xFF, 0xEF, 0xC0, 0x44, 0xFF, 0xEF, 0xBF, 0x43, 0xFF, 0xF0, 0xBE, 0x41, + 0xFF, 0xEE, 0xBD, 0x42, 0xFF, 0xEC, 0xBC, 0x43, 0xFF, 0xEF, 0xBC, 0x40, 0xFF, 0xF1, 0xBB, 0x3E, 0xFF, 0xFD, 0xC0, + 0x2F, 0xFF, 0xFB, 0xBD, 0x35, 0xFF, 0x00, 0x4B, 0xF5, 0xFF, 0x52, 0x8A, 0xFF, 0xFF, 0x5D, 0xA5, 0xFA, 0xFF, 0x8D, + 0xC4, 0xFC, 0xFF, 0x85, 0xC1, 0xFB, 0xFF, 0x50, 0xAD, 0xF5, 0xFF, 0x5E, 0xB6, 0xF7, 0xFF, 0x6B, 0xBE, 0xF9, 0xFF, + 0x78, 0xC9, 0xFA, 0xFF, 0x85, 0xD4, 0xFB, 0xFF, 0x97, 0xDE, 0xFE, 0xFF, 0xAA, 0xE8, 0xFF, 0xFF, 0xAD, 0xEE, 0xFD, + 0xFF, 0xB1, 0xF4, 0xF9, 0xFF, 0xB9, 0xF5, 0xFC, 0xFF, 0xC2, 0xF6, 0xFE, 0xFF, 0xB2, 0xF0, 0xFB, 0xFF, 0x6E, 0xCB, + 0xF6, 0xFF, 0x91, 0xDE, 0xFB, 0xFF, 0xCA, 0xFC, 0xFC, 0xFF, 0xD0, 0xFB, 0xFF, 0xFF, 0xC8, 0xFC, 0xFF, 0xFF, 0xC7, + 0xE3, 0xCA, 0xFF, 0xF2, 0xA1, 0x15, 0xFF, 0xEE, 0xA3, 0x1D, 0xFF, 0xF1, 0xA1, 0x11, 0xFF, 0xB9, 0xD4, 0x9E, 0xFF, + 0x8B, 0xF1, 0xEA, 0xFF, 0x95, 0xEF, 0xDC, 0xFF, 0x90, 0xEB, 0xD9, 0xFF, 0x92, 0xEB, 0xD9, 0xFF, 0x94, 0xEC, 0xD8, + 0xFF, 0x8B, 0xEB, 0xD6, 0xFF, 0x82, 0xEA, 0xD3, 0xFF, 0x78, 0xE6, 0xC9, 0xFF, 0x6F, 0xE3, 0xBF, 0xFF, 0x68, 0xE2, + 0xB8, 0xFF, 0x61, 0xE2, 0xB1, 0xFF, 0x5D, 0xE0, 0xAE, 0xFF, 0x5A, 0xDE, 0xAC, 0xFF, 0x51, 0xD9, 0xA2, 0xFF, 0x48, + 0xD3, 0x98, 0xFF, 0x41, 0xCB, 0x8E, 0xFF, 0x39, 0xC3, 0x83, 0xFF, 0x32, 0xB7, 0x74, 0xFF, 0x2C, 0xAC, 0x66, 0xFF, + 0x29, 0xA2, 0x5D, 0xFF, 0x26, 0x99, 0x54, 0xFF, 0x21, 0x93, 0x4A, 0xFF, 0xB9, 0x99, 0x23, 0xFF, 0xFE, 0x93, 0x15, + 0xFF, 0xD8, 0x92, 0x09, 0xFF, 0xD8, 0x8F, 0x0F, 0xFF, 0xEF, 0xC4, 0x49, 0xFF, 0xEF, 0xC3, 0x47, 0xFF, 0xF0, 0xC2, + 0x46, 0xFF, 0xF0, 0xC1, 0x44, 0xFF, 0xF1, 0xC0, 0x42, 0xFF, 0xEF, 0xBF, 0x43, 0xFF, 0xED, 0xBE, 0x43, 0xFF, 0xEE, + 0xBE, 0x42, 0xFF, 0xF0, 0xBD, 0x41, 0xFF, 0xF0, 0xBA, 0x37, 0xFF, 0xB7, 0xA1, 0x71, 0xFF, 0x1D, 0x5D, 0xFE, 0xFF, + 0x31, 0x79, 0xF8, 0xFF, 0x51, 0xA1, 0xF5, 0xFF, 0x60, 0xAD, 0xF8, 0xFF, 0xBC, 0xE0, 0xFE, 0xFF, 0x57, 0xB6, 0xF7, + 0xFF, 0x68, 0xBF, 0xF9, 0xFF, 0x79, 0xC8, 0xFC, 0xFF, 0x82, 0xD2, 0xFC, 0xFF, 0x8B, 0xDB, 0xFC, 0xFF, 0x8F, 0xDE, + 0xFB, 0xFF, 0x92, 0xE0, 0xFB, 0xFF, 0xA3, 0xEA, 0xFA, 0xFF, 0xB4, 0xF4, 0xFA, 0xFF, 0xC1, 0xF8, 0xF9, 0xFF, 0xCE, + 0xFB, 0xF8, 0xFF, 0xB6, 0xEB, 0xF9, 0xFF, 0x83, 0xE1, 0xFA, 0xFF, 0x8F, 0xE2, 0xFD, 0xFF, 0xC7, 0xF9, 0xFB, 0xFF, + 0xD7, 0xF8, 0xFC, 0xFF, 0xCA, 0xFC, 0xFE, 0xFF, 0xDC, 0xCD, 0x8B, 0xFF, 0xED, 0x9F, 0x18, 0xFF, 0xED, 0xA3, 0x24, + 0xFF, 0xFA, 0x9D, 0x0A, 0xFF, 0xA5, 0xF5, 0xE7, 0xFF, 0xA5, 0xF1, 0xE4, 0xFF, 0xA5, 0xF0, 0xE4, 0xFF, 0xA6, 0xEF, + 0xE3, 0xFF, 0xA9, 0xF0, 0xE4, 0xFF, 0xAD, 0xF2, 0xE6, 0xFF, 0xA5, 0xF0, 0xE3, 0xFF, 0x9E, 0xEF, 0xE0, 0xFF, 0x92, + 0xEC, 0xD6, 0xFF, 0x87, 0xE9, 0xCD, 0xFF, 0x7F, 0xE9, 0xC7, 0xFF, 0x78, 0xEA, 0xC2, 0xFF, 0x72, 0xEA, 0xC1, 0xFF, + 0x6D, 0xE9, 0xC0, 0xFF, 0x60, 0xE3, 0xB1, 0xFF, 0x53, 0xDD, 0xA2, 0xFF, 0x46, 0xD2, 0x94, 0xFF, 0x3A, 0xC8, 0x86, + 0xFF, 0x35, 0xBC, 0x77, 0xFF, 0x30, 0xB0, 0x69, 0xFF, 0x2B, 0xA5, 0x62, 0xFF, 0x26, 0x9B, 0x5B, 0xFF, 0x09, 0x91, + 0x57, 0xFF, 0xFB, 0x94, 0x09, 0xFF, 0xE5, 0x95, 0x0C, 0xFF, 0xEB, 0x91, 0x0F, 0xFF, 0xEB, 0x91, 0x0F, 0xFF, 0xEF, + 0xC5, 0x4A, 0xFF, 0xF0, 0xC4, 0x48, 0xFF, 0xF0, 0xC3, 0x47, 0xFF, 0xF1, 0xC2, 0x45, 0xFF, 0xF1, 0xC1, 0x43, 0xFF, + 0xF1, 0xC1, 0x41, 0xFF, 0xF1, 0xC1, 0x3F, 0xFF, 0xF0, 0xBE, 0x3F, 0xFF, 0xEF, 0xBC, 0x3F, 0xFF, 0xFD, 0xC2, 0x32, + 0xFF, 0x6E, 0x7F, 0xBD, 0xFF, 0x26, 0x65, 0xFE, 0xFF, 0x34, 0x7B, 0xF5, 0xFF, 0x4C, 0x9A, 0xF5, 0xFF, 0x5C, 0xAB, + 0xF8, 0xFF, 0x9F, 0xD0, 0xFA, 0xFF, 0x83, 0xC6, 0xF7, 0xFF, 0x6A, 0xC1, 0xFD, 0xFF, 0x7E, 0xD1, 0xFD, 0xFF, 0x87, + 0xDB, 0xFB, 0xFF, 0x8F, 0xE5, 0xF9, 0xFF, 0x9A, 0xEC, 0xF8, 0xFF, 0xA5, 0xF4, 0xF7, 0xFF, 0x99, 0xEA, 0xFB, 0xFF, + 0x8E, 0xDF, 0xFF, 0xFF, 0x9F, 0xE2, 0xFB, 0xFF, 0xB1, 0xE6, 0xF7, 0xFF, 0xCC, 0xED, 0xFB, 0xFF, 0xCA, 0xFA, 0xFF, + 0xFF, 0xC6, 0xF2, 0xFF, 0xFF, 0xC2, 0xF0, 0xFC, 0xFF, 0xD2, 0xF5, 0xFE, 0xFF, 0xD3, 0xFC, 0xFF, 0xFF, 0xE6, 0xB5, + 0x4B, 0xFF, 0xED, 0xA4, 0x20, 0xFF, 0xED, 0xA2, 0x1B, 0xFF, 0xE2, 0xAA, 0x3D, 0xFF, 0xAB, 0xF6, 0xEE, 0xFF, 0xB1, + 0xF1, 0xE5, 0xFF, 0xB4, 0xF2, 0xE7, 0xFF, 0xB8, 0xF3, 0xE9, 0xFF, 0xBA, 0xF3, 0xE9, 0xFF, 0xBC, 0xF4, 0xEA, 0xFF, + 0xB5, 0xF3, 0xE8, 0xFF, 0xAF, 0xF2, 0xE5, 0xFF, 0xA8, 0xF0, 0xE0, 0xFF, 0xA1, 0xED, 0xDA, 0xFF, 0x99, 0xEF, 0xD5, + 0xFF, 0x91, 0xF0, 0xD0, 0xFF, 0x82, 0xED, 0xC8, 0xFF, 0x72, 0xEA, 0xC0, 0xFF, 0x61, 0xE3, 0xB0, 0xFF, 0x50, 0xDC, + 0xA0, 0xFF, 0x47, 0xD3, 0x94, 0xFF, 0x3E, 0xCA, 0x88, 0xFF, 0x38, 0xBF, 0x7B, 0xFF, 0x32, 0xB4, 0x6E, 0xFF, 0x2E, + 0xA8, 0x65, 0xFF, 0x1B, 0xA0, 0x5D, 0xFF, 0x48, 0x94, 0x3C, 0xFF, 0xF6, 0x93, 0x0A, 0xFF, 0xEC, 0x94, 0x0D, 0xFF, + 0xF0, 0x92, 0x10, 0xFF, 0xF0, 0x92, 0x10, 0xFF, 0xF0, 0xC5, 0x4B, 0xFF, 0xF0, 0xC4, 0x49, 0xFF, 0xF1, 0xC4, 0x48, + 0xFF, 0xF1, 0xC3, 0x46, 0xFF, 0xF2, 0xC2, 0x44, 0xFF, 0xF4, 0xC3, 0x3F, 0xFF, 0xF6, 0xC4, 0x3A, 0xFF, 0xF3, 0xBF, + 0x3C, 0xFF, 0xEF, 0xBA, 0x3D, 0xFF, 0xFF, 0xCA, 0x2C, 0xFF, 0x24, 0x5D, 0xFF, 0xFF, 0x2E, 0x6D, 0xFE, 0xFF, 0x38, + 0x7D, 0xF2, 0xFF, 0x48, 0x93, 0xF5, 0xFF, 0x57, 0xA9, 0xF7, 0xFF, 0x82, 0xC0, 0xF7, 0xFF, 0xAE, 0xD7, 0xF7, 0xFF, + 0x6C, 0xC2, 0xFF, 0xFF, 0x84, 0xDA, 0xFE, 0xFF, 0x8B, 0xE4, 0xFA, 0xFF, 0x93, 0xEE, 0xF6, 0xFF, 0x9D, 0xED, 0xF8, + 0xFF, 0xA7, 0xEC, 0xF9, 0xFF, 0xB3, 0xF1, 0xF8, 0xFF, 0xC0, 0xF6, 0xF7, 0xFF, 0xC8, 0xF6, 0xFB, 0xFF, 0xD0, 0xF6, + 0xFF, 0xFF, 0xD3, 0xF2, 0xFE, 0xFF, 0xB9, 0xF3, 0xFB, 0xFF, 0xE7, 0xFD, 0xFF, 0xFF, 0xE9, 0xFD, 0xF6, 0xFF, 0xE2, + 0xFC, 0xFC, 0xFF, 0xDC, 0xFC, 0xFF, 0xFF, 0xF1, 0x9D, 0x0B, 0xFF, 0xEC, 0xAA, 0x29, 0xFF, 0xF5, 0xAA, 0x1B, 0xFF, + 0xD9, 0xC7, 0x7F, 0xFF, 0xBA, 0xFE, 0xFD, 0xFF, 0xBD, 0xF2, 0xE7, 0xFF, 0xC3, 0xF4, 0xEB, 0xFF, 0xCA, 0xF6, 0xEE, + 0xFF, 0xCA, 0xF6, 0xEF, 0xFF, 0xCB, 0xF7, 0xEF, 0xFF, 0xC5, 0xF6, 0xED, 0xFF, 0xBF, 0xF5, 0xEB, 0xFF, 0xBE, 0xF3, + 0xE9, 0xFF, 0xBC, 0xF2, 0xE8, 0xFF, 0xB3, 0xF4, 0xE3, 0xFF, 0xAB, 0xF6, 0xDF, 0xFF, 0x91, 0xF1, 0xD0, 0xFF, 0x77, + 0xEC, 0xC1, 0xFF, 0x62, 0xE3, 0xAF, 0xFF, 0x4E, 0xDB, 0x9E, 0xFF, 0x47, 0xD3, 0x94, 0xFF, 0x41, 0xCC, 0x8A, 0xFF, + 0x3B, 0xC2, 0x7F, 0xFF, 0x35, 0xB8, 0x73, 0xFF, 0x30, 0xAC, 0x69, 0xFF, 0x10, 0xA5, 0x60, 0xFF, 0x86, 0x96, 0x22, + 0xFF, 0xF0, 0x91, 0x0A, 0xFF, 0xF2, 0x92, 0x0E, 0xFF, 0xF4, 0x94, 0x11, 0xFF, 0xF4, 0x94, 0x11, 0xFF, 0xF1, 0xC5, + 0x4C, 0xFF, 0xF1, 0xC5, 0x4A, 0xFF, 0xF1, 0xC4, 0x49, 0xFF, 0xF2, 0xC4, 0x47, 0xFF, 0xF2, 0xC3, 0x45, 0xFF, 0xF1, + 0xC3, 0x43, 0xFF, 0xF0, 0xC4, 0x40, 0xFF, 0xF3, 0xBF, 0x42, 0xFF, 0xF5, 0xC0, 0x39, 0xFF, 0xCA, 0xAC, 0x5E, 0xFF, + 0x1E, 0x58, 0xFA, 0xFF, 0x30, 0x6E, 0xF3, 0xFF, 0x35, 0x80, 0xF7, 0xFF, 0x3E, 0x92, 0xFB, 0xFF, 0x5D, 0xAF, 0xFB, + 0xFF, 0x72, 0xC2, 0xFF, 0xFF, 0xBA, 0xE1, 0xFD, 0xFF, 0x74, 0xCD, 0xFF, 0xFF, 0x71, 0xD3, 0xFF, 0xFF, 0x83, 0xE5, + 0xFF, 0xFF, 0x95, 0xF7, 0xFF, 0xFF, 0xA1, 0xF4, 0xFE, 0xFF, 0xAD, 0xF0, 0xFD, 0xFF, 0xC1, 0xF8, 0xFF, 0xFF, 0xCD, + 0xF7, 0xFB, 0xFF, 0xD1, 0xF8, 0xFE, 0xFF, 0xD6, 0xF9, 0xFF, 0xFF, 0xE0, 0xF6, 0xFE, 0xFF, 0xDD, 0xF5, 0xFB, 0xFF, + 0xED, 0xFB, 0xFF, 0xFF, 0xE8, 0xFB, 0xFB, 0xFF, 0xDF, 0xFC, 0xFF, 0xFF, 0xE8, 0xE0, 0xB2, 0xFF, 0xEF, 0xA3, 0x18, + 0xFF, 0xEC, 0xAA, 0x25, 0xFF, 0xF5, 0xA8, 0x15, 0xFF, 0xD8, 0xE3, 0xC2, 0xFF, 0xC5, 0xF9, 0xF9, 0xFF, 0xCA, 0xF5, + 0xEE, 0xFF, 0xCE, 0xF6, 0xEF, 0xFF, 0xD2, 0xF7, 0xF0, 0xFF, 0xD1, 0xF8, 0xF1, 0xFF, 0xD0, 0xF9, 0xF1, 0xFF, 0xCD, + 0xF9, 0xF1, 0xFF, 0xC9, 0xF9, 0xF1, 0xFF, 0xC9, 0xFB, 0xF2, 0xFF, 0xCA, 0xFC, 0xF4, 0xFF, 0xB6, 0xF8, 0xE6, 0xFF, + 0xA2, 0xF3, 0xD9, 0xFF, 0x89, 0xEF, 0xCA, 0xFF, 0x71, 0xEB, 0xBC, 0xFF, 0x61, 0xE6, 0xB0, 0xFF, 0x50, 0xE1, 0xA4, + 0xFF, 0x48, 0xD9, 0x99, 0xFF, 0x40, 0xD2, 0x8F, 0xFF, 0x3A, 0xC7, 0x83, 0xFF, 0x34, 0xBC, 0x77, 0xFF, 0x1C, 0xB2, + 0x6A, 0xFF, 0x04, 0xA9, 0x5D, 0xFF, 0xEA, 0x8D, 0x13, 0xFF, 0xEF, 0x93, 0x11, 0xFF, 0xEF, 0x92, 0x0F, 0xFF, 0xF0, + 0x92, 0x0E, 0xFF, 0xF0, 0x92, 0x0E, 0xFF, 0xF2, 0xC6, 0x4D, 0xFF, 0xF2, 0xC5, 0x4B, 0xFF, 0xF2, 0xC5, 0x4A, 0xFF, + 0xF2, 0xC5, 0x48, 0xFF, 0xF2, 0xC4, 0x46, 0xFF, 0xEE, 0xC4, 0x46, 0xFF, 0xEA, 0xC4, 0x46, 0xFF, 0xF2, 0xBF, 0x48, + 0xFF, 0xFB, 0xC6, 0x34, 0xFF, 0x91, 0x95, 0x98, 0xFF, 0x27, 0x64, 0xFC, 0xFF, 0x3B, 0x76, 0xF1, 0xFF, 0x32, 0x83, + 0xFC, 0xFF, 0x34, 0x91, 0xFF, 0xFF, 0x63, 0xB4, 0xFF, 0xFF, 0x5A, 0xBD, 0xFF, 0xFF, 0xB5, 0xDC, 0xF3, 0xFF, 0x97, + 0xD0, 0xCB, 0xFF, 0xA4, 0xCE, 0xB4, 0xFF, 0xB0, 0xD2, 0xAF, 0xFF, 0xBC, 0xD6, 0xAB, 0xFF, 0xBE, 0xE1, 0xC2, 0xFF, + 0xC0, 0xEB, 0xDA, 0xFF, 0xC7, 0xFC, 0xF5, 0xFF, 0xBD, 0xFE, 0xFF, 0xFF, 0xCC, 0xFD, 0xFF, 0xFF, 0xDB, 0xFC, 0xFF, + 0xFF, 0xE0, 0xFC, 0xFE, 0xFF, 0xE4, 0xFC, 0xFB, 0xFF, 0xE6, 0xFB, 0xFD, 0xFF, 0xE7, 0xFA, 0xFF, 0xFF, 0xDD, 0xFB, + 0xFF, 0xFF, 0xF4, 0xC4, 0x61, 0xFF, 0xEE, 0xAA, 0x26, 0xFF, 0xEB, 0xAA, 0x22, 0xFF, 0xF6, 0xA7, 0x10, 0xFF, 0xD6, + 0xFF, 0xFF, 0xFF, 0xCF, 0xF4, 0xF5, 0xFF, 0xD8, 0xF9, 0xF5, 0xFF, 0xD9, 0xF9, 0xF4, 0xFF, 0xD9, 0xF8, 0xF2, 0xFF, + 0xD8, 0xF9, 0xF3, 0xFF, 0xD6, 0xFB, 0xF4, 0xFF, 0xD5, 0xFC, 0xF5, 0xFF, 0xD3, 0xFD, 0xF6, 0xFF, 0xCD, 0xFA, 0xF3, + 0xFF, 0xC7, 0xF6, 0xEF, 0xFF, 0xB0, 0xF3, 0xE1, 0xFF, 0x98, 0xF0, 0xD3, 0xFF, 0x82, 0xED, 0xC5, 0xFF, 0x6B, 0xEB, + 0xB7, 0xFF, 0x5F, 0xE9, 0xB0, 0xFF, 0x53, 0xE7, 0xAA, 0xFF, 0x49, 0xDF, 0x9F, 0xFF, 0x3E, 0xD7, 0x93, 0xFF, 0x39, + 0xCB, 0x87, 0xFF, 0x34, 0xBF, 0x7B, 0xFF, 0x25, 0xB4, 0x6B, 0xFF, 0x32, 0xA2, 0x5B, 0xFF, 0xF9, 0x94, 0x04, 0xFF, + 0xED, 0x94, 0x17, 0xFF, 0xEC, 0x92, 0x11, 0xFF, 0xEB, 0x91, 0x0B, 0xFF, 0xEB, 0x91, 0x0B, 0xFF, 0xF2, 0xC7, 0x4E, + 0xFF, 0xF3, 0xC7, 0x4D, 0xFF, 0xF3, 0xC7, 0x4C, 0xFF, 0xF3, 0xC7, 0x4A, 0xFF, 0xF4, 0xC7, 0x49, 0xFF, 0xF1, 0xC4, + 0x47, 0xFF, 0xEE, 0xC1, 0x45, 0xFF, 0xF7, 0xC2, 0x42, 0xFF, 0xFF, 0xC8, 0x33, 0xFF, 0x46, 0x67, 0xDE, 0xFF, 0x2A, + 0x63, 0xFF, 0xFF, 0x1B, 0x6F, 0xFF, 0xFF, 0x52, 0x8B, 0xE0, 0xFF, 0x84, 0xA0, 0xA3, 0xFF, 0xCC, 0xC1, 0x62, 0xFF, + 0xFF, 0xC0, 0x26, 0xFF, 0xFF, 0xB7, 0x29, 0xFF, 0xF1, 0xB5, 0x24, 0xFF, 0xF9, 0xB7, 0x27, 0xFF, 0xF6, 0xB5, 0x25, + 0xFF, 0xF2, 0xB2, 0x23, 0xFF, 0xFA, 0xB5, 0x24, 0xFF, 0xFF, 0xB7, 0x24, 0xFF, 0xDE, 0x9D, 0x17, 0xFF, 0xF4, 0xBA, + 0x42, 0xFF, 0xE7, 0xDA, 0x9E, 0xFF, 0xDC, 0xF9, 0xF9, 0xFF, 0xE6, 0xFB, 0xF3, 0xFF, 0xE9, 0xFF, 0xFF, 0xFF, 0xE6, + 0xFF, 0xFD, 0xFF, 0xE2, 0xFB, 0xFA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xA7, 0x1D, 0xFF, 0xF0, 0xA7, 0x1C, 0xFF, + 0xF1, 0xA7, 0x1A, 0xFF, 0xF0, 0xC4, 0x5A, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xE1, 0xF9, 0xFA, 0xFF, 0xE2, 0xFB, 0xFA, + 0xFF, 0xDF, 0xFB, 0xF8, 0xFF, 0xDC, 0xFA, 0xF5, 0xFF, 0xDB, 0xFB, 0xF5, 0xFF, 0xD9, 0xFB, 0xF5, 0xFF, 0xD6, 0xFC, + 0xF5, 0xFF, 0xD3, 0xFD, 0xF5, 0xFF, 0xC8, 0xF8, 0xF0, 0xFF, 0xBD, 0xF4, 0xEA, 0xFF, 0xA8, 0xF1, 0xDF, 0xFF, 0x93, + 0xEF, 0xD4, 0xFF, 0x7A, 0xF3, 0xC6, 0xFF, 0x61, 0xF8, 0xB9, 0xFF, 0x57, 0xEF, 0xB0, 0xFF, 0x4D, 0xE6, 0xA6, 0xFF, + 0x48, 0xE2, 0xA3, 0xFF, 0x3A, 0xD6, 0x98, 0xFF, 0x37, 0xCD, 0x89, 0xFF, 0x35, 0xC3, 0x7B, 0xFF, 0x20, 0xB7, 0x6F, + 0xFF, 0x84, 0x9C, 0x3A, 0xFF, 0xF4, 0x93, 0x0C, 0xFF, 0xEC, 0x94, 0x13, 0xFF, 0xE9, 0x93, 0x11, 0xFF, 0xE6, 0x92, + 0x0F, 0xFF, 0xE6, 0x92, 0x0F, 0xFF, 0xF3, 0xC9, 0x50, 0xFF, 0xF4, 0xC9, 0x4F, 0xFF, 0xF4, 0xC9, 0x4E, 0xFF, 0xF5, + 0xCA, 0x4D, 0xFF, 0xF5, 0xCA, 0x4B, 0xFF, 0xF4, 0xC5, 0x48, 0xFF, 0xF3, 0xBF, 0x44, 0xFF, 0xEE, 0xC1, 0x47, 0xFF, + 0xEA, 0xC4, 0x4A, 0xFF, 0x1F, 0x52, 0xFF, 0xFF, 0x92, 0x9A, 0xA6, 0xFF, 0xE6, 0xB6, 0x51, 0xFF, 0xFF, 0xC7, 0x28, + 0xFF, 0xF8, 0xC4, 0x2C, 0xFF, 0xF0, 0xC0, 0x30, 0xFF, 0xEF, 0xBA, 0x3F, 0xFF, 0xEF, 0xBF, 0x37, 0xFF, 0xEF, 0xB9, + 0x38, 0xFF, 0xF0, 0xB2, 0x3A, 0xFF, 0xF3, 0xB5, 0x38, 0xFF, 0xF6, 0xB7, 0x35, 0xFF, 0xEF, 0xB9, 0x32, 0xFF, 0xE8, + 0xBB, 0x2F, 0xFF, 0xEA, 0xB8, 0x2F, 0xFF, 0xED, 0xB4, 0x2F, 0xFF, 0xF3, 0xAC, 0x1F, 0xFF, 0xF9, 0xA3, 0x10, 0xFF, + 0xF2, 0xC9, 0x6F, 0xFF, 0xDF, 0xF9, 0xF5, 0xFF, 0xDE, 0xFB, 0xF5, 0xFF, 0xDD, 0xFD, 0xF5, 0xFF, 0xE3, 0xEA, 0xD7, + 0xFF, 0xEE, 0xA5, 0x10, 0xFF, 0xF4, 0xB2, 0x2D, 0xFF, 0xF7, 0xA5, 0x13, 0xFF, 0xEB, 0xE1, 0xA5, 0xFF, 0xF8, 0xFF, + 0xFF, 0xFF, 0xF2, 0xFE, 0xFF, 0xFF, 0xEC, 0xFD, 0xFF, 0xFF, 0xE6, 0xFC, 0xFC, 0xFF, 0xDF, 0xFC, 0xF7, 0xFF, 0xDE, + 0xFC, 0xF7, 0xFF, 0xDC, 0xFC, 0xF6, 0xFF, 0xD7, 0xFC, 0xF5, 0xFF, 0xD3, 0xFC, 0xF4, 0xFF, 0xC3, 0xF7, 0xED, 0xFF, + 0xB4, 0xF1, 0xE5, 0xFF, 0xB7, 0xF5, 0xE4, 0xFF, 0xBB, 0xF9, 0xE4, 0xFF, 0xD2, 0xFE, 0xEB, 0xFF, 0xE9, 0xFF, 0xF2, + 0xFF, 0xDB, 0xFE, 0xED, 0xFF, 0xCD, 0xF9, 0xE8, 0xFF, 0x89, 0xEF, 0xCA, 0xFF, 0x35, 0xD6, 0x9C, 0xFF, 0x2D, 0xC6, + 0x83, 0xFF, 0x25, 0xB7, 0x6B, 0xFF, 0x14, 0xB3, 0x6C, 0xFF, 0xD6, 0x95, 0x1A, 0xFF, 0xEE, 0x91, 0x15, 0xFF, 0xEB, + 0x93, 0x0F, 0xFF, 0xE6, 0x93, 0x10, 0xFF, 0xE0, 0x93, 0x12, 0xFF, 0xE0, 0x93, 0x12, 0xFF, 0xF4, 0xCA, 0x52, 0xFF, + 0xF4, 0xCA, 0x50, 0xFF, 0xF3, 0xCA, 0x4E, 0xFF, 0xF3, 0xC9, 0x4C, 0xFF, 0xF3, 0xC9, 0x4A, 0xFF, 0xF4, 0xC8, 0x48, + 0xFF, 0xF6, 0xC6, 0x46, 0xFF, 0xEC, 0xBF, 0x3F, 0xFF, 0xEB, 0xBF, 0x41, 0xFF, 0xF8, 0xD4, 0x40, 0xFF, 0xFC, 0xC9, + 0x33, 0xFF, 0xFF, 0xC9, 0x2F, 0xFF, 0xEC, 0xC2, 0x42, 0xFF, 0xF4, 0xC3, 0x40, 0xFF, 0xFC, 0xC3, 0x3E, 0xFF, 0xF3, + 0xBB, 0x34, 0xFF, 0xF2, 0xBB, 0x33, 0xFF, 0xF6, 0xBD, 0x49, 0xFF, 0xF8, 0xB7, 0x38, 0xFF, 0xF5, 0xB7, 0x36, 0xFF, + 0xF2, 0xB7, 0x34, 0xFF, 0xF3, 0xB5, 0x2E, 0xFF, 0xF5, 0xB3, 0x27, 0xFF, 0xF7, 0xBA, 0x2F, 0xFF, 0xF2, 0xBA, 0x2F, + 0xFF, 0xF1, 0xB5, 0x30, 0xFF, 0xF0, 0xB0, 0x31, 0xFF, 0xF6, 0xAC, 0x1E, 0xFF, 0xED, 0xAA, 0x0C, 0xFF, 0xEC, 0xD2, + 0x7E, 0xFF, 0xE6, 0xFF, 0xFF, 0xFF, 0xD2, 0xD9, 0x80, 0xFF, 0xF8, 0xA9, 0x2E, 0xFF, 0xEB, 0xAF, 0x1C, 0xFF, 0xE5, + 0xAA, 0x02, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xF9, 0xFE, 0xFF, 0xFF, 0xF4, 0xFD, 0xFF, 0xFF, + 0xEB, 0xFD, 0xFD, 0xFF, 0xE2, 0xFE, 0xFA, 0xFF, 0xE1, 0xFD, 0xF9, 0xFF, 0xE0, 0xFC, 0xF7, 0xFF, 0xD7, 0xFC, 0xF5, + 0xFF, 0xCF, 0xFD, 0xF3, 0xFF, 0xE2, 0xFB, 0xF4, 0xFF, 0xE7, 0xFD, 0xF6, 0xFF, 0xE8, 0xFD, 0xF3, 0xFF, 0xE9, 0xFD, + 0xF0, 0xFF, 0xD3, 0xFD, 0xEB, 0xFF, 0xBD, 0xFC, 0xE5, 0xFF, 0xBA, 0xF7, 0xDF, 0xFF, 0xB6, 0xF2, 0xDA, 0xFF, 0xD2, + 0xFB, 0xE9, 0xFF, 0xE6, 0xFC, 0xF1, 0xFF, 0x8D, 0xDE, 0xB6, 0xFF, 0x3C, 0xC7, 0x84, 0xFF, 0x47, 0xB7, 0x99, 0xFF, + 0xF8, 0xA1, 0x13, 0xFF, 0xF2, 0x94, 0x04, 0xFF, 0xEE, 0x94, 0x10, 0xFF, 0xEC, 0x94, 0x10, 0xFF, 0xE9, 0x95, 0x10, + 0xFF, 0xE9, 0x95, 0x10, 0xFF, 0xF5, 0xCC, 0x53, 0xFF, 0xF3, 0xCB, 0x50, 0xFF, 0xF2, 0xCA, 0x4E, 0xFF, 0xF1, 0xC9, + 0x4B, 0xFF, 0xF0, 0xC7, 0x48, 0xFF, 0xF4, 0xCB, 0x48, 0xFF, 0xF9, 0xCE, 0x47, 0xFF, 0xF2, 0xC4, 0x40, 0xFF, 0xFC, + 0xCA, 0x48, 0xFF, 0xF0, 0xC2, 0x3F, 0xFF, 0xF5, 0xC9, 0x46, 0xFF, 0xF4, 0xC7, 0x46, 0xFF, 0xF3, 0xC4, 0x45, 0xFF, + 0xED, 0xB4, 0x38, 0xFF, 0xE8, 0xA5, 0x2C, 0xFF, 0xE1, 0xB0, 0x2E, 0xFF, 0xEA, 0xC0, 0x56, 0xFF, 0xE9, 0xC8, 0x6C, + 0xFF, 0xE4, 0xC1, 0x36, 0xFF, 0xEB, 0xC9, 0x50, 0xFF, 0xF1, 0xD1, 0x6A, 0xFF, 0xF5, 0xD0, 0x73, 0xFF, 0xF9, 0xCF, + 0x7D, 0xFF, 0xF8, 0xC7, 0x56, 0xFF, 0xE7, 0xAF, 0x1F, 0xFF, 0xED, 0xB1, 0x25, 0xFF, 0xF4, 0xB2, 0x2B, 0xFF, 0xF9, + 0xB5, 0x3E, 0xFF, 0xEE, 0xB3, 0x2A, 0xFF, 0xF5, 0xAF, 0x1B, 0xFF, 0xF0, 0xB5, 0x32, 0xFF, 0xF9, 0xB1, 0x3F, 0xFF, + 0xF2, 0xA9, 0x26, 0xFF, 0xEA, 0xAE, 0x1F, 0xFF, 0xF3, 0xB8, 0x3F, 0xFF, 0xF3, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFB, 0xFD, 0xFE, 0xFF, 0xF0, 0xFE, 0xFE, 0xFF, 0xE5, 0xFF, 0xFD, 0xFF, 0xE4, 0xFE, + 0xFB, 0xFF, 0xE3, 0xFC, 0xF8, 0xFF, 0xD7, 0xFD, 0xF5, 0xFF, 0xCB, 0xFD, 0xF2, 0xFF, 0xEB, 0xFB, 0xF4, 0xFF, 0xEE, + 0xFE, 0xF6, 0xFF, 0xDE, 0xFD, 0xF1, 0xFF, 0xCE, 0xFB, 0xED, 0xFF, 0xB0, 0xF9, 0xE2, 0xFF, 0x91, 0xF6, 0xD8, 0xFF, + 0x8A, 0xF3, 0xD2, 0xFF, 0x83, 0xF1, 0xCC, 0xFF, 0x96, 0xEE, 0xCE, 0xFF, 0xA9, 0xEA, 0xD0, 0xFF, 0xC0, 0xEA, 0xDA, + 0xFF, 0xE8, 0xFA, 0xF4, 0xFF, 0x78, 0xC6, 0x7E, 0xFF, 0xFF, 0xC0, 0x59, 0xFF, 0xEA, 0xA0, 0x19, 0xFF, 0xF2, 0x95, + 0x10, 0xFF, 0xF2, 0x96, 0x0F, 0xFF, 0xF2, 0x96, 0x0D, 0xFF, 0xF2, 0x96, 0x0D, 0xFF, 0xF4, 0xCD, 0x54, 0xFF, 0xF4, + 0xCB, 0x51, 0xFF, 0xF3, 0xCA, 0x4F, 0xFF, 0xF2, 0xC9, 0x4C, 0xFF, 0xF2, 0xC8, 0x4A, 0xFF, 0xF1, 0xC6, 0x48, 0xFF, + 0xF1, 0xC4, 0x47, 0xFF, 0xF3, 0xD2, 0x48, 0xFF, 0xF3, 0xC7, 0x46, 0xFF, 0xFB, 0xC5, 0x4C, 0xFF, 0xDC, 0x9A, 0x2B, + 0xFF, 0xCD, 0x83, 0x17, 0xFF, 0xBE, 0x6B, 0x03, 0xFF, 0xC5, 0x7F, 0x00, 0xFF, 0xD4, 0x96, 0x0E, 0xFF, 0xDB, 0xAC, + 0x2E, 0xFF, 0xEA, 0xC5, 0x60, 0xFF, 0xEF, 0xCC, 0x75, 0xFF, 0xEA, 0xCA, 0x51, 0xFF, 0xEF, 0xD2, 0x69, 0xFF, 0xF5, + 0xDA, 0x81, 0xFF, 0xF7, 0xE4, 0x99, 0xFF, 0xF9, 0xEE, 0xB2, 0xFF, 0xFF, 0xFA, 0xCE, 0xFF, 0xFF, 0xFE, 0xE2, 0xFF, + 0xFF, 0xE1, 0x99, 0xFF, 0xF7, 0xBC, 0x48, 0xFF, 0xDC, 0xB4, 0x10, 0xFF, 0xF0, 0xAD, 0x31, 0xFF, 0xFB, 0xAC, 0x27, + 0xFF, 0xF3, 0xB2, 0x30, 0xFF, 0xF5, 0xB1, 0x34, 0xFF, 0xF0, 0xAD, 0x24, 0xFF, 0xF6, 0xAC, 0x26, 0xFF, 0xFC, 0xD1, + 0x97, 0xFF, 0xF7, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFB, 0xFF, 0xFE, 0xFF, 0xF3, 0xFF, 0xFE, 0xFF, 0xED, + 0xFF, 0xFD, 0xFF, 0xE7, 0xFD, 0xFC, 0xFF, 0xE3, 0xFE, 0xFB, 0xFF, 0xDF, 0xFE, 0xF9, 0xFF, 0xE7, 0xFD, 0xF8, 0xFF, + 0xEF, 0xFC, 0xF7, 0xFF, 0xEB, 0xFB, 0xF3, 0xFF, 0xD8, 0xFD, 0xEF, 0xFF, 0xC2, 0xFA, 0xE8, 0xFF, 0xAB, 0xF8, 0xE2, + 0xFF, 0x9B, 0xF4, 0xD8, 0xFF, 0x8A, 0xEF, 0xCE, 0xFF, 0x76, 0xEA, 0xC1, 0xFF, 0x61, 0xE5, 0xB4, 0xFF, 0x5A, 0xDD, + 0xAB, 0xFF, 0x61, 0xD2, 0xA2, 0xFF, 0x8D, 0xE9, 0xC1, 0xFF, 0xB8, 0xE7, 0xDA, 0xFF, 0xFF, 0xD4, 0x96, 0xFF, 0xFA, + 0xD0, 0x8E, 0xFF, 0xED, 0xAD, 0x41, 0xFF, 0xF1, 0x95, 0x10, 0xFF, 0xF1, 0x95, 0x0F, 0xFF, 0xF1, 0x96, 0x0E, 0xFF, + 0xF1, 0x96, 0x0E, 0xFF, 0xF4, 0xCD, 0x54, 0xFF, 0xF4, 0xCC, 0x52, 0xFF, 0xF4, 0xCB, 0x50, 0xFF, 0xF3, 0xC9, 0x4E, + 0xFF, 0xF3, 0xC8, 0x4B, 0xFF, 0xF6, 0xC9, 0x51, 0xFF, 0xFA, 0xCA, 0x56, 0xFF, 0xEA, 0xC0, 0x44, 0xFF, 0xC6, 0x74, + 0x19, 0xFF, 0xAD, 0x58, 0x00, 0xFF, 0xB3, 0x5B, 0x01, 0xFF, 0xC0, 0x6F, 0x06, 0xFF, 0xCC, 0x84, 0x0B, 0xFF, 0xCE, + 0x93, 0x00, 0xFF, 0xDF, 0xA7, 0x11, 0xFF, 0xE5, 0xB9, 0x3E, 0xFF, 0xEB, 0xCA, 0x6A, 0xFF, 0xF5, 0xD1, 0x7E, 0xFF, + 0xF0, 0xD3, 0x6B, 0xFF, 0xF4, 0xDB, 0x81, 0xFF, 0xF8, 0xE3, 0x97, 0xFF, 0xF7, 0xEB, 0xA4, 0xFF, 0xF5, 0xF4, 0xB1, + 0xFF, 0xF9, 0xF7, 0xC7, 0xFF, 0xFC, 0xFA, 0xDC, 0xFF, 0xFF, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xF8, 0xFF, 0xFD, 0xEB, + 0xBB, 0xFF, 0xF2, 0xB4, 0x22, 0xFF, 0xFF, 0xAF, 0x28, 0xFF, 0xF6, 0xB0, 0x2F, 0xFF, 0xF2, 0xB0, 0x29, 0xFF, 0xEE, + 0xB1, 0x22, 0xFF, 0xF9, 0xA7, 0x19, 0xFF, 0xF4, 0xE6, 0xC9, 0xFF, 0xF4, 0xF7, 0xF7, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, + 0xF6, 0xFF, 0xFE, 0xFF, 0xEC, 0xFF, 0xFD, 0xFF, 0xEA, 0xFF, 0xFC, 0xFF, 0xE8, 0xFA, 0xFA, 0xFF, 0xE2, 0xFD, 0xFB, + 0xFF, 0xDC, 0xFF, 0xFB, 0xFF, 0xE9, 0xFF, 0xFB, 0xFF, 0xF6, 0xFF, 0xFB, 0xFF, 0xDC, 0xFD, 0xF1, 0xFF, 0xC3, 0xFB, + 0xE7, 0xFF, 0xB4, 0xF5, 0xDF, 0xFF, 0xA5, 0xF0, 0xD8, 0xFF, 0x94, 0xEC, 0xCE, 0xFF, 0x83, 0xE8, 0xC4, 0xFF, 0x77, + 0xE5, 0xB7, 0xFF, 0x6B, 0xE3, 0xAB, 0xFF, 0x52, 0xDE, 0xA0, 0xFF, 0x55, 0xD4, 0x94, 0xFF, 0x40, 0xBD, 0x7F, 0xFF, + 0x98, 0xE4, 0xD1, 0xFF, 0xF4, 0xA1, 0x2B, 0xFF, 0xF6, 0xA1, 0x2F, 0xFF, 0xF3, 0x9B, 0x1F, 0xFF, 0xF0, 0x95, 0x0F, + 0xFF, 0xF0, 0x95, 0x0F, 0xFF, 0xF0, 0x95, 0x0F, 0xFF, 0xF0, 0x95, 0x0F, 0xFF, 0xF4, 0xCE, 0x55, 0xFF, 0xF4, 0xCC, + 0x53, 0xFF, 0xF4, 0xCB, 0x51, 0xFF, 0xF5, 0xCA, 0x4F, 0xFF, 0xF6, 0xC9, 0x4E, 0xFF, 0xF4, 0xC9, 0x4D, 0xFF, 0xFA, + 0xD0, 0x53, 0xFF, 0xCD, 0x86, 0x2A, 0xFF, 0xB0, 0x52, 0x06, 0xFF, 0xB8, 0x5F, 0x04, 0xFF, 0xC8, 0x73, 0x0A, 0xFF, + 0xCE, 0x82, 0x08, 0xFF, 0xD3, 0x91, 0x06, 0xFF, 0xD5, 0xA0, 0x01, 0xFF, 0xE6, 0xB4, 0x24, 0xFF, 0xEA, 0xC4, 0x4C, + 0xFF, 0xED, 0xD3, 0x74, 0xFF, 0xF4, 0xD9, 0x83, 0xFF, 0xF3, 0xDC, 0x7E, 0xFF, 0xF6, 0xE4, 0x93, 0xFF, 0xF8, 0xEC, + 0xA8, 0xFF, 0xF9, 0xF2, 0xB5, 0xFF, 0xF9, 0xF8, 0xC3, 0xFF, 0xFA, 0xFA, 0xD3, 0xFF, 0xFB, 0xFB, 0xE2, 0xFF, 0xFB, + 0xFE, 0xED, 0xFF, 0xF3, 0xF9, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xEE, 0xDC, 0x7E, 0xFF, + 0xFD, 0xAD, 0x26, 0xFF, 0xF7, 0xAF, 0x29, 0xFF, 0xF1, 0xB1, 0x2D, 0xFF, 0xDF, 0xB1, 0x34, 0xFF, 0xF6, 0xA6, 0x09, + 0xFF, 0xF4, 0xD3, 0x8C, 0xFF, 0xF8, 0xFB, 0xFC, 0xFF, 0xF6, 0xFF, 0xFF, 0xFF, 0xEB, 0xFF, 0xFD, 0xFF, 0xE5, 0xFE, + 0xFC, 0xFF, 0xE0, 0xFB, 0xFB, 0xFF, 0xDE, 0xFC, 0xF9, 0xFF, 0xDC, 0xFC, 0xF7, 0xFF, 0xEF, 0xFF, 0xFC, 0xFF, 0xEB, + 0xFC, 0xF8, 0xFF, 0xD0, 0xF5, 0xE8, 0xFF, 0xBC, 0xF5, 0xDF, 0xFF, 0xAC, 0xF1, 0xD8, 0xFF, 0x9D, 0xED, 0xD2, 0xFF, + 0x7D, 0xE8, 0xC4, 0xFF, 0x6C, 0xE1, 0xB7, 0xFF, 0x5E, 0xDC, 0xAB, 0xFF, 0x4F, 0xD7, 0x9E, 0xFF, 0x5E, 0xC9, 0x98, + 0xFF, 0x35, 0xC6, 0x92, 0xFF, 0x42, 0xC9, 0x8B, 0xFF, 0x4D, 0xB2, 0x80, 0xFF, 0xF1, 0x9B, 0x00, 0xFF, 0xF8, 0x93, + 0x17, 0xFF, 0xF4, 0x95, 0x15, 0xFF, 0xF1, 0x97, 0x12, 0xFF, 0xF0, 0x96, 0x11, 0xFF, 0xEF, 0x95, 0x10, 0xFF, 0xEF, + 0x95, 0x10, 0xFF, 0xF4, 0xCE, 0x55, 0xFF, 0xF4, 0xCD, 0x54, 0xFF, 0xF5, 0xCC, 0x52, 0xFF, 0xF6, 0xCB, 0x51, 0xFF, + 0xF8, 0xCB, 0x50, 0xFF, 0xF1, 0xC8, 0x49, 0xFF, 0xF9, 0xD5, 0x51, 0xFF, 0xC0, 0x62, 0x15, 0xFF, 0xBB, 0x5C, 0x00, + 0xFF, 0xCC, 0x74, 0x07, 0xFF, 0xCD, 0x7C, 0x02, 0xFF, 0xD4, 0x8D, 0x02, 0xFF, 0xDB, 0x9E, 0x01, 0xFF, 0xDC, 0xAD, + 0x08, 0xFF, 0xED, 0xC1, 0x36, 0xFF, 0xEE, 0xCF, 0x5A, 0xFF, 0xF0, 0xDC, 0x7D, 0xFF, 0xF3, 0xE1, 0x87, 0xFF, 0xF7, + 0xE6, 0x91, 0xFF, 0xF8, 0xED, 0xA5, 0xFF, 0xF8, 0xF5, 0xB8, 0xFF, 0xFB, 0xF9, 0xC6, 0xFF, 0xFD, 0xFD, 0xD4, 0xFF, + 0xFB, 0xFC, 0xDF, 0xFF, 0xFA, 0xFC, 0xE9, 0xFF, 0xFD, 0xFE, 0xF0, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFE, 0xFF, 0xFA, + 0xFF, 0xFB, 0xFE, 0xFC, 0xFF, 0xFF, 0xFA, 0xFD, 0xFF, 0xE7, 0xAF, 0x1D, 0xFF, 0xEE, 0xB0, 0x2A, 0xFF, 0xF5, 0xB1, + 0x37, 0xFF, 0xF6, 0xB8, 0x24, 0xFF, 0xF7, 0xB4, 0x28, 0xFF, 0xF4, 0xAF, 0x21, 0xFF, 0xF2, 0xAA, 0x1A, 0xFF, 0xF5, + 0xD7, 0x9E, 0xFF, 0xE9, 0xFF, 0xFC, 0xFF, 0xE0, 0xFE, 0xFC, 0xFF, 0xD7, 0xFD, 0xFC, 0xFF, 0xDA, 0xFA, 0xF8, 0xFF, + 0xDD, 0xF7, 0xF3, 0xFF, 0xF4, 0xFD, 0xFD, 0xFF, 0xE0, 0xF9, 0xF6, 0xFF, 0xC3, 0xEC, 0xDF, 0xFF, 0xB5, 0xEF, 0xD7, + 0xFF, 0xA5, 0xEC, 0xD2, 0xFF, 0x95, 0xE9, 0xCC, 0xFF, 0x67, 0xE5, 0xBB, 0xFF, 0x55, 0xDB, 0xAB, 0xFF, 0x44, 0xD3, + 0x9E, 0xFF, 0x32, 0xCB, 0x91, 0xFF, 0x24, 0xC8, 0x85, 0xFF, 0x6A, 0xB4, 0x79, 0xFF, 0xAF, 0x9D, 0x3A, 0xFF, 0xFF, + 0x97, 0x0B, 0xFF, 0xF9, 0x93, 0x18, 0xFF, 0xED, 0x9B, 0x0F, 0xFF, 0xF0, 0x9A, 0x12, 0xFF, 0xF3, 0x98, 0x15, 0xFF, + 0xF1, 0x96, 0x13, 0xFF, 0xEF, 0x94, 0x11, 0xFF, 0xEF, 0x94, 0x11, 0xFF, 0xF4, 0xCF, 0x58, 0xFF, 0xF4, 0xCE, 0x55, + 0xFF, 0xF4, 0xCD, 0x53, 0xFF, 0xF6, 0xCC, 0x52, 0xFF, 0xF8, 0xCB, 0x52, 0xFF, 0xFA, 0xD5, 0x52, 0xFF, 0xFB, 0xC7, + 0x4E, 0xFF, 0xAD, 0x4C, 0x00, 0xFF, 0xCA, 0x6F, 0x09, 0xFF, 0xD3, 0x7F, 0x0B, 0xFF, 0xD4, 0x88, 0x05, 0xFF, 0xDB, + 0x97, 0x04, 0xFF, 0xE1, 0xA7, 0x04, 0xFF, 0xE5, 0xB6, 0x18, 0xFF, 0xF1, 0xC7, 0x3F, 0xFF, 0xF3, 0xD3, 0x62, 0xFF, + 0xF4, 0xDF, 0x86, 0xFF, 0xF7, 0xE4, 0x91, 0xFF, 0xF9, 0xE9, 0x9B, 0xFF, 0xF9, 0xF0, 0xAD, 0xFF, 0xF9, 0xF7, 0xBF, + 0xFF, 0xFB, 0xFA, 0xCB, 0xFF, 0xFD, 0xFC, 0xD7, 0xFF, 0xFC, 0xFD, 0xDE, 0xFF, 0xFB, 0xFD, 0xE5, 0xFF, 0xFE, 0xFF, + 0xEF, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFA, 0xFE, 0xF2, 0xFF, 0xFC, 0xFE, 0xFE, 0xFF, 0xFB, 0xE9, 0xC6, 0xFF, 0xEC, + 0xAF, 0x1D, 0xFF, 0xF6, 0xB4, 0x30, 0xFF, 0xF8, 0xB6, 0x2F, 0xFF, 0xF6, 0xA7, 0x19, 0xFF, 0xF0, 0xB0, 0x26, 0xFF, + 0xF2, 0xAD, 0x22, 0xFF, 0xF5, 0xAB, 0x1D, 0xFF, 0xF9, 0xA9, 0x26, 0xFF, 0xF6, 0xA6, 0x1C, 0xFF, 0xE9, 0xCD, 0x7D, + 0xFF, 0xDC, 0xF4, 0xDF, 0xFF, 0xAF, 0xFE, 0xEA, 0xFF, 0xED, 0xFD, 0xFD, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xD3, 0xF8, + 0xFB, 0xFF, 0xB4, 0xEE, 0xEC, 0xFF, 0xAB, 0xE9, 0xE6, 0xFF, 0x89, 0xE6, 0xD8, 0xFF, 0x67, 0xE2, 0xCB, 0xFF, 0x52, + 0xE1, 0xB8, 0xFF, 0x4C, 0xDD, 0xA6, 0xFF, 0x7E, 0xC5, 0x74, 0xFF, 0xB0, 0xAD, 0x42, 0xFF, 0xF3, 0x9B, 0x22, 0xFF, + 0xFF, 0x9C, 0x09, 0xFF, 0xF5, 0x98, 0x09, 0xFF, 0xEE, 0x9C, 0x10, 0xFF, 0xED, 0x99, 0x17, 0xFF, 0xED, 0x9D, 0x14, + 0xFF, 0xEF, 0x9B, 0x14, 0xFF, 0xF2, 0x99, 0x15, 0xFF, 0xF0, 0x97, 0x13, 0xFF, 0xEE, 0x95, 0x11, 0xFF, 0xEE, 0x95, + 0x11, 0xFF, 0xF5, 0xD0, 0x5A, 0xFF, 0xF4, 0xCF, 0x57, 0xFF, 0xF3, 0xCE, 0x54, 0xFF, 0xF5, 0xCC, 0x53, 0xFF, 0xF7, + 0xCB, 0x53, 0xFF, 0xF4, 0xD3, 0x4C, 0xFF, 0xDD, 0x9A, 0x2C, 0xFF, 0xC1, 0x5D, 0x03, 0xFF, 0xC8, 0x72, 0x05, 0xFF, + 0xD2, 0x83, 0x06, 0xFF, 0xDC, 0x93, 0x07, 0xFF, 0xE1, 0xA2, 0x07, 0xFF, 0xE7, 0xB0, 0x08, 0xFF, 0xEE, 0xBF, 0x27, + 0xFF, 0xF6, 0xCD, 0x47, 0xFF, 0xF7, 0xD8, 0x6B, 0xFF, 0xF9, 0xE2, 0x8E, 0xFF, 0xFA, 0xE7, 0x9A, 0xFF, 0xFB, 0xEC, + 0xA6, 0xFF, 0xFA, 0xF3, 0xB6, 0xFF, 0xFA, 0xF9, 0xC7, 0xFF, 0xFB, 0xFB, 0xD0, 0xFF, 0xFD, 0xFC, 0xD9, 0xFF, 0xFC, + 0xFD, 0xDD, 0xFF, 0xFC, 0xFE, 0xE2, 0xFF, 0xFE, 0xFF, 0xEE, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xF7, 0xFD, 0xEA, 0xFF, + 0xFE, 0xFE, 0xFF, 0xFF, 0xF7, 0xD7, 0x8F, 0xFF, 0xF1, 0xAF, 0x1E, 0xFF, 0xF6, 0xB0, 0x2E, 0xFF, 0xEB, 0xAB, 0x17, + 0xFF, 0xFD, 0xF7, 0xDF, 0xFF, 0xE9, 0xAC, 0x24, 0xFF, 0xF0, 0xAC, 0x22, 0xFF, 0xF8, 0xAC, 0x21, 0xFF, 0xF6, 0xAE, + 0x26, 0xFF, 0xF5, 0xB0, 0x2B, 0xFF, 0xF4, 0xA9, 0x19, 0xFF, 0xF3, 0xA2, 0x08, 0xFF, 0xF9, 0xA7, 0x22, 0xFF, 0xF2, + 0xC1, 0x4C, 0xFF, 0xEE, 0xCD, 0x6D, 0xFF, 0xDB, 0xC9, 0x7D, 0xFF, 0xC2, 0xCA, 0x7F, 0xFF, 0xC6, 0xC5, 0x81, 0xFF, + 0xCB, 0xBC, 0x60, 0xFF, 0xCF, 0xB3, 0x40, 0xFF, 0xE9, 0xA7, 0x24, 0xFF, 0xFF, 0x9B, 0x07, 0xFF, 0xFF, 0x9D, 0x10, + 0xFF, 0xFF, 0x9F, 0x1A, 0xFF, 0xE9, 0x98, 0x0F, 0xFF, 0xF9, 0x9C, 0x14, 0xFF, 0xF7, 0x9C, 0x14, 0xFF, 0xF4, 0x9B, + 0x14, 0xFF, 0xF1, 0x9D, 0x17, 0xFF, 0xED, 0x9E, 0x19, 0xFF, 0xEF, 0x9C, 0x16, 0xFF, 0xF1, 0x99, 0x14, 0xFF, 0xEF, + 0x97, 0x12, 0xFF, 0xED, 0x95, 0x10, 0xFF, 0xED, 0x95, 0x10, 0xFF, 0xF6, 0xD1, 0x5C, 0xFF, 0xF4, 0xD0, 0x58, 0xFF, + 0xF3, 0xCF, 0x55, 0xFF, 0xF5, 0xCD, 0x54, 0xFF, 0xF7, 0xCC, 0x53, 0xFF, 0xF6, 0xD5, 0x51, 0xFF, 0xCE, 0x7B, 0x16, + 0xFF, 0xC6, 0x67, 0x03, 0xFF, 0xCF, 0x7B, 0x06, 0xFF, 0xD7, 0x8B, 0x05, 0xFF, 0xDF, 0x9B, 0x05, 0xFF, 0xE4, 0xA8, + 0x07, 0xFF, 0xEA, 0xB6, 0x09, 0xFF, 0xF1, 0xC3, 0x2A, 0xFF, 0xF7, 0xD1, 0x4C, 0xFF, 0xF8, 0xDB, 0x6C, 0xFF, 0xFA, + 0xE4, 0x8D, 0xFF, 0xFA, 0xEA, 0x9C, 0xFF, 0xFB, 0xEF, 0xAB, 0xFF, 0xFA, 0xF5, 0xBC, 0xFF, 0xFA, 0xFA, 0xCD, 0xFF, + 0xFB, 0xFB, 0xD4, 0xFF, 0xFD, 0xFC, 0xDB, 0xFF, 0xFC, 0xFD, 0xDC, 0xFF, 0xFC, 0xFE, 0xDD, 0xFF, 0xFC, 0xFE, 0xE3, + 0xFF, 0xFD, 0xFE, 0xEA, 0xFF, 0xFD, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDE, 0xC0, 0x27, 0xFF, 0xF5, 0xB4, + 0x26, 0xFF, 0xF8, 0xB0, 0x1E, 0xFF, 0xFF, 0xC6, 0x4D, 0xFF, 0xEF, 0xF8, 0xFF, 0xFF, 0xFA, 0xFF, 0xFE, 0xFF, 0xF6, + 0xD8, 0x8B, 0xFF, 0xF3, 0xA7, 0x18, 0xFF, 0xF4, 0xA9, 0x1D, 0xFF, 0xF5, 0xAC, 0x22, 0xFF, 0xF2, 0xAB, 0x22, 0xFF, + 0xF0, 0xAB, 0x22, 0xFF, 0xF2, 0xA3, 0x1A, 0xFF, 0xEE, 0xA6, 0x1A, 0xFF, 0xF4, 0xA8, 0x17, 0xFF, 0xF3, 0xA2, 0x0D, + 0xFF, 0xF2, 0xA4, 0x10, 0xFF, 0xFF, 0xA3, 0x14, 0xFF, 0xFC, 0xA3, 0x15, 0xFF, 0xF9, 0xA2, 0x16, 0xFF, 0xF2, 0xA2, + 0x17, 0xFF, 0xEC, 0xA1, 0x18, 0xFF, 0xFD, 0x99, 0x0D, 0xFF, 0xED, 0x9A, 0x16, 0xFF, 0xFF, 0xA0, 0x00, 0xFF, 0xE8, + 0x9C, 0x2B, 0xFF, 0xAF, 0xB5, 0x60, 0xFF, 0xF7, 0x99, 0x10, 0xFF, 0xF2, 0x9B, 0x14, 0xFF, 0xED, 0x9D, 0x18, 0xFF, + 0xEE, 0x9B, 0x16, 0xFF, 0xEF, 0x99, 0x13, 0xFF, 0xED, 0x97, 0x11, 0xFF, 0xEB, 0x95, 0x0F, 0xFF, 0xEB, 0x95, 0x0F, + 0xFF, 0xF7, 0xD2, 0x5E, 0xFF, 0xF4, 0xD1, 0x5A, 0xFF, 0xF2, 0xD0, 0x56, 0xFF, 0xF5, 0xCE, 0x54, 0xFF, 0xF7, 0xCC, + 0x53, 0xFF, 0xF7, 0xD7, 0x56, 0xFF, 0xC0, 0x5B, 0x00, 0xFF, 0xCB, 0x70, 0x03, 0xFF, 0xD6, 0x84, 0x06, 0xFF, 0xDC, + 0x94, 0x05, 0xFF, 0xE2, 0xA3, 0x03, 0xFF, 0xE8, 0xAF, 0x07, 0xFF, 0xEE, 0xBB, 0x0B, 0xFF, 0xF3, 0xC8, 0x2D, 0xFF, + 0xF8, 0xD5, 0x50, 0xFF, 0xF9, 0xDE, 0x6E, 0xFF, 0xFA, 0xE6, 0x8C, 0xFF, 0xFB, 0xEC, 0x9F, 0xFF, 0xFB, 0xF2, 0xB1, + 0xFF, 0xFA, 0xF7, 0xC2, 0xFF, 0xF9, 0xFB, 0xD3, 0xFF, 0xFB, 0xFC, 0xD8, 0xFF, 0xFD, 0xFC, 0xDD, 0xFF, 0xFC, 0xFD, + 0xDB, 0xFF, 0xFC, 0xFE, 0xD8, 0xFF, 0xFB, 0xFD, 0xD8, 0xFF, 0xFA, 0xFC, 0xD9, 0xFF, 0xFA, 0xFA, 0xE4, 0xFF, 0xF6, + 0xE9, 0xA3, 0xFF, 0xFB, 0xAC, 0x2A, 0xFF, 0xFA, 0xB9, 0x2E, 0xFF, 0xED, 0xAD, 0x1A, 0xFF, 0xF7, 0xDA, 0x99, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFD, 0xFE, 0xFF, 0xFC, 0xFE, 0xFE, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xF9, 0xD4, 0x8C, + 0xFF, 0xF5, 0xA8, 0x19, 0xFF, 0xF7, 0xA9, 0x17, 0xFF, 0xF8, 0xA9, 0x16, 0xFF, 0xF3, 0xA7, 0x1A, 0xFF, 0xED, 0xA5, + 0x1E, 0xFF, 0xF1, 0xA7, 0x1F, 0xFF, 0xF6, 0xA9, 0x20, 0xFF, 0xF6, 0xA7, 0x1D, 0xFF, 0xF6, 0xA5, 0x1A, 0xFF, 0xF8, + 0xA3, 0x16, 0xFF, 0xFA, 0xA1, 0x12, 0xFF, 0xFC, 0x9D, 0x0A, 0xFF, 0xFE, 0x98, 0x03, 0xFF, 0xF9, 0xA1, 0x25, 0xFF, + 0xB0, 0xC0, 0x6F, 0xFF, 0x5D, 0xC9, 0xCF, 0xFF, 0x27, 0xE5, 0xFF, 0xFF, 0xB3, 0xB4, 0x73, 0xFF, 0xF9, 0x97, 0x0B, + 0xFF, 0xF3, 0x9A, 0x11, 0xFF, 0xED, 0x9D, 0x17, 0xFF, 0xEE, 0x9B, 0x15, 0xFF, 0xEE, 0x9A, 0x13, 0xFF, 0xEC, 0x98, + 0x11, 0xFF, 0xEA, 0x96, 0x0F, 0xFF, 0xEA, 0x96, 0x0F, 0xFF, 0xF6, 0xD1, 0x5D, 0xFF, 0xF5, 0xD1, 0x5A, 0xFF, 0xF4, + 0xD2, 0x58, 0xFF, 0xF3, 0xCE, 0x53, 0xFF, 0xFA, 0xD1, 0x56, 0xFF, 0xE6, 0xB1, 0x3F, 0xFF, 0xC6, 0x64, 0x01, 0xFF, + 0xCE, 0x75, 0x02, 0xFF, 0xD7, 0x87, 0x04, 0xFF, 0xDD, 0x95, 0x02, 0xFF, 0xE4, 0xA4, 0x00, 0xFF, 0xEA, 0xB0, 0x03, + 0xFF, 0xF1, 0xBD, 0x06, 0xFF, 0xF2, 0xC8, 0x1B, 0xFF, 0xFB, 0xD5, 0x42, 0xFF, 0xFB, 0xDD, 0x63, 0xFF, 0xFB, 0xE5, + 0x84, 0xFF, 0xFC, 0xEB, 0x98, 0xFF, 0xFC, 0xF1, 0xAB, 0xFF, 0xFF, 0xF8, 0xBD, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, + 0xFC, 0xCF, 0xFF, 0xFB, 0xF9, 0xCF, 0xFF, 0xFD, 0xFE, 0xD2, 0xFF, 0xFF, 0xFF, 0xD4, 0xFF, 0xFF, 0xF9, 0xC6, 0xFF, + 0xFF, 0xEE, 0xB7, 0xFF, 0xD9, 0xD7, 0x59, 0xFF, 0xE9, 0xB9, 0x40, 0xFF, 0xFF, 0xB9, 0x2E, 0xFF, 0xEF, 0xB1, 0x2B, + 0xFF, 0xEB, 0xAF, 0x27, 0xFF, 0xF1, 0xEF, 0xDD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0xFD, 0xFE, + 0xFF, 0xFF, 0xF9, 0xFD, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xF0, 0xE8, 0xC1, 0xFF, 0xE6, + 0xCD, 0x83, 0xFF, 0xE8, 0xBB, 0x52, 0xFF, 0xEB, 0xA9, 0x21, 0xFF, 0xFF, 0xA1, 0x13, 0xFF, 0xF7, 0x9F, 0x06, 0xFF, + 0xF8, 0x9F, 0x0F, 0xFF, 0xEA, 0xA3, 0x18, 0xFF, 0xE0, 0xB1, 0x43, 0xFF, 0xC9, 0xC2, 0x6D, 0xFF, 0x99, 0xD6, 0xAF, + 0xFF, 0x6A, 0xEB, 0xF1, 0xFF, 0x31, 0xEE, 0xEB, 0xFF, 0x47, 0xE6, 0xF8, 0xFF, 0x3A, 0xE1, 0xFF, 0xFF, 0x41, 0xE1, + 0xFC, 0xFF, 0xF4, 0x98, 0x00, 0xFF, 0xFC, 0xA1, 0x19, 0xFF, 0xF6, 0x9E, 0x15, 0xFF, 0xF1, 0x9A, 0x11, 0xFF, 0xF0, + 0x9A, 0x13, 0xFF, 0xF0, 0x99, 0x14, 0xFF, 0xEE, 0x97, 0x12, 0xFF, 0xEC, 0x95, 0x10, 0xFF, 0xEC, 0x95, 0x10, 0xFF, + 0xF5, 0xCF, 0x5C, 0xFF, 0xF6, 0xD1, 0x5A, 0xFF, 0xF6, 0xD4, 0x59, 0xFF, 0xF2, 0xCD, 0x51, 0xFF, 0xFE, 0xD6, 0x59, + 0xFF, 0xD5, 0x8B, 0x29, 0xFF, 0xCB, 0x6C, 0x02, 0xFF, 0xD1, 0x7A, 0x01, 0xFF, 0xD8, 0x89, 0x01, 0xFF, 0xDE, 0x97, + 0x00, 0xFF, 0xE5, 0xA5, 0x00, 0xFF, 0xEC, 0xB2, 0x00, 0xFF, 0xF3, 0xBE, 0x02, 0xFF, 0xF1, 0xC7, 0x08, 0xFF, 0xFE, + 0xD5, 0x35, 0xFF, 0xFD, 0xDD, 0x58, 0xFF, 0xFB, 0xE4, 0x7C, 0xFF, 0xFC, 0xEA, 0x91, 0xFF, 0xFE, 0xEF, 0xA6, 0xFF, + 0xFE, 0xF2, 0xB0, 0xFF, 0xFE, 0xF5, 0xBA, 0xFF, 0xFC, 0xF5, 0xBD, 0xFF, 0xF9, 0xF5, 0xC0, 0xFF, 0xF6, 0xF7, 0xC0, + 0xFF, 0xF4, 0xF9, 0xC1, 0xFF, 0xFC, 0xFC, 0xC6, 0xFF, 0xFF, 0xFF, 0xCC, 0xFF, 0xF7, 0xF8, 0xC1, 0xFF, 0xF3, 0xCC, + 0x59, 0xFF, 0xF2, 0xB0, 0x38, 0xFF, 0xF5, 0xBA, 0x37, 0xFF, 0xF7, 0xB4, 0x29, 0xFF, 0xF8, 0xFB, 0xFC, 0xFF, 0xFF, + 0xFD, 0xFC, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xF6, 0xFC, 0xFF, 0xFF, 0xF2, 0xFE, 0xFC, 0xFF, + 0xEE, 0xFF, 0xF6, 0xFF, 0xE9, 0xFF, 0xFC, 0xFF, 0xE4, 0xFF, 0xFF, 0xFF, 0xD7, 0xFF, 0xFF, 0xFF, 0xCA, 0xFF, 0xFF, + 0xFF, 0xF1, 0xFB, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFD, 0xFC, 0xFF, 0x88, 0xFF, 0xF6, 0xFF, 0x91, 0xFD, + 0xFB, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0x6C, 0xFC, 0xFC, 0xFF, 0x59, 0xF6, 0xF9, 0xFF, 0x58, 0xEF, 0xF8, 0xFF, 0x57, + 0xE9, 0xF7, 0xFF, 0x59, 0xE3, 0xF6, 0xFF, 0x67, 0xD2, 0xD0, 0xFF, 0xFF, 0x98, 0x08, 0xFF, 0xEF, 0x9A, 0x17, 0xFF, + 0xF1, 0x99, 0x12, 0xFF, 0xF4, 0x98, 0x0C, 0xFF, 0xF3, 0x99, 0x10, 0xFF, 0xF1, 0x99, 0x15, 0xFF, 0xEF, 0x97, 0x13, + 0xFF, 0xED, 0x95, 0x11, 0xFF, 0xED, 0x95, 0x11, 0xFF, 0xF9, 0xD1, 0x5E, 0xFF, 0xF7, 0xD3, 0x5B, 0xFF, 0xF6, 0xD4, + 0x59, 0xFF, 0xF8, 0xD3, 0x57, 0xFF, 0xFF, 0xDA, 0x5E, 0xFF, 0xCD, 0x70, 0x19, 0xFF, 0xCC, 0x6D, 0x02, 0xFF, 0xD2, + 0x7B, 0x03, 0xFF, 0xD9, 0x88, 0x04, 0xFF, 0xDF, 0x96, 0x04, 0xFF, 0xE6, 0xA5, 0x04, 0xFF, 0xE6, 0xAD, 0x01, 0xFF, + 0xE7, 0xB4, 0x00, 0xFF, 0xEA, 0xBE, 0x06, 0xFF, 0xF5, 0xCA, 0x23, 0xFF, 0xF8, 0xD7, 0x4B, 0xFF, 0xFB, 0xE3, 0x74, + 0xFF, 0xFC, 0xE8, 0x89, 0xFF, 0xFE, 0xEC, 0x9E, 0xFF, 0xFE, 0xED, 0xA5, 0xFF, 0xFE, 0xEE, 0xAB, 0xFF, 0xFB, 0xEF, + 0xAD, 0xFF, 0xF9, 0xEF, 0xB0, 0xFF, 0xF8, 0xF2, 0xB3, 0xFF, 0xF8, 0xF5, 0xB6, 0xFF, 0xFC, 0xF8, 0xB5, 0xFF, 0xFF, + 0xFB, 0xB5, 0xFF, 0xFF, 0xF3, 0xD9, 0xFF, 0xF1, 0xB9, 0x1A, 0xFF, 0xF3, 0xB3, 0x28, 0xFF, 0xF6, 0xB3, 0x2A, 0xFF, + 0xF3, 0xCE, 0x73, 0xFF, 0xF5, 0xFD, 0xFD, 0xFF, 0xF9, 0xFE, 0xFD, 0xFF, 0xFE, 0xFF, 0xFD, 0xFF, 0xF8, 0xFE, 0xFF, + 0xFF, 0xF3, 0xFD, 0xFF, 0xFF, 0xEE, 0xFE, 0xFD, 0xFF, 0xE9, 0xFE, 0xFA, 0xFF, 0xE3, 0xFF, 0xFC, 0xFF, 0xDE, 0xFF, + 0xFF, 0xFF, 0xD0, 0xFF, 0xFF, 0xFF, 0xC2, 0xFF, 0xFF, 0xFF, 0xD6, 0xFA, 0xFC, 0xFF, 0xF3, 0xFC, 0xFF, 0xFF, 0xBF, + 0xFF, 0xFE, 0xFF, 0xC4, 0xFA, 0xFC, 0xFF, 0x84, 0xFF, 0xFC, 0xFF, 0x8A, 0xFA, 0xFB, 0xFF, 0x79, 0xF6, 0xFA, 0xFF, + 0x68, 0xF2, 0xF9, 0xFF, 0x5E, 0xED, 0xF6, 0xFF, 0x53, 0xE8, 0xF4, 0xFF, 0x48, 0xE8, 0xF7, 0xFF, 0xA8, 0xBC, 0x87, + 0xFF, 0xFB, 0x9A, 0x10, 0xFF, 0xF1, 0x9B, 0x17, 0xFF, 0xF1, 0x9A, 0x14, 0xFF, 0xF1, 0x9A, 0x10, 0xFF, 0xF2, 0x99, + 0x13, 0xFF, 0xF3, 0x98, 0x16, 0xFF, 0xF1, 0x96, 0x14, 0xFF, 0xEF, 0x94, 0x12, 0xFF, 0xEF, 0x94, 0x12, 0xFF, 0xFC, + 0xD4, 0x61, 0xFF, 0xF9, 0xD4, 0x5D, 0xFF, 0xF6, 0xD4, 0x58, 0xFF, 0xF5, 0xD1, 0x55, 0xFF, 0xF5, 0xCE, 0x53, 0xFF, + 0xBD, 0x4D, 0x01, 0xFF, 0xCD, 0x6E, 0x02, 0xFF, 0xD3, 0x7B, 0x04, 0xFF, 0xDA, 0x87, 0x06, 0xFF, 0xE0, 0x96, 0x09, + 0xFF, 0xE6, 0xA5, 0x0C, 0xFF, 0xE9, 0xB0, 0x0A, 0xFF, 0xEB, 0xBA, 0x09, 0xFF, 0xF3, 0xC5, 0x15, 0xFF, 0xFB, 0xD0, + 0x21, 0xFF, 0xFB, 0xD9, 0x46, 0xFF, 0xFB, 0xE3, 0x6B, 0xFF, 0xFC, 0xE6, 0x81, 0xFF, 0xFD, 0xE9, 0x97, 0xFF, 0xFD, + 0xE8, 0x99, 0xFF, 0xFD, 0xE8, 0x9B, 0xFF, 0xFB, 0xE8, 0x9D, 0xFF, 0xF8, 0xE9, 0x9F, 0xFF, 0xFA, 0xEE, 0xA5, 0xFF, + 0xFC, 0xF2, 0xAB, 0xFF, 0xFB, 0xEF, 0xB0, 0xFF, 0xFB, 0xEB, 0xB4, 0xFF, 0xF8, 0xDD, 0x89, 0xFF, 0xF3, 0xB4, 0x27, + 0xFF, 0xF7, 0xBD, 0x3E, 0xFF, 0xF7, 0xAC, 0x1E, 0xFF, 0xF0, 0xE8, 0xBC, 0xFF, 0xF2, 0xFF, 0xFE, 0xFF, 0xF3, 0xFF, + 0xFD, 0xFF, 0xF4, 0xFE, 0xFD, 0xFF, 0xF2, 0xFE, 0xFD, 0xFF, 0xEF, 0xFE, 0xFE, 0xFF, 0xE9, 0xFE, 0xFE, 0xFF, 0xE3, + 0xFE, 0xFE, 0xFF, 0xDD, 0xFD, 0xFD, 0xFF, 0xD7, 0xFD, 0xFD, 0xFF, 0xC8, 0xFE, 0xFC, 0xFF, 0xB9, 0xFF, 0xFB, 0xFF, + 0x9F, 0xFE, 0xF5, 0xFF, 0xCE, 0xFF, 0xFF, 0xFF, 0xF5, 0xF9, 0xFF, 0xFF, 0xC8, 0xFF, 0xFF, 0xFF, 0xBD, 0xF7, 0xFC, + 0xFF, 0x7A, 0xF8, 0xF8, 0xFF, 0x6B, 0xF5, 0xF8, 0xFF, 0x5C, 0xF3, 0xF9, 0xFF, 0x55, 0xED, 0xF5, 0xFF, 0x4F, 0xE8, + 0xF1, 0xFF, 0x37, 0xEE, 0xF8, 0xFF, 0xE9, 0xA6, 0x3E, 0xFF, 0xF5, 0x9C, 0x17, 0xFF, 0xF4, 0x9D, 0x17, 0xFF, 0xF1, + 0x9C, 0x15, 0xFF, 0xEE, 0x9B, 0x14, 0xFF, 0xF1, 0x99, 0x15, 0xFF, 0xF4, 0x97, 0x17, 0xFF, 0xF2, 0x95, 0x15, 0xFF, + 0xF0, 0x93, 0x13, 0xFF, 0xF0, 0x93, 0x13, 0xFF, 0xFB, 0xD6, 0x66, 0xFF, 0xF4, 0xD1, 0x5E, 0xFF, 0xF6, 0xD3, 0x5F, + 0xFF, 0xF9, 0xD7, 0x59, 0xFF, 0xDA, 0x9D, 0x39, 0xFF, 0xBE, 0x58, 0x08, 0xFF, 0xCD, 0x6C, 0x08, 0xFF, 0xD2, 0x79, + 0x0C, 0xFF, 0xD7, 0x87, 0x0F, 0xFF, 0xDF, 0x96, 0x11, 0xFF, 0xE7, 0xA5, 0x13, 0xFF, 0xEA, 0xB0, 0x13, 0xFF, 0xF5, + 0xC2, 0x1B, 0xFF, 0xF3, 0xC8, 0x0F, 0xFF, 0xF9, 0xD0, 0x16, 0xFF, 0xF4, 0xD2, 0x27, 0xFF, 0xF7, 0xD6, 0x4B, 0xFF, + 0xF8, 0xDA, 0x60, 0xFF, 0xF9, 0xDE, 0x76, 0xFF, 0xF9, 0xDF, 0x7F, 0xFF, 0xFA, 0xE0, 0x87, 0xFF, 0xFB, 0xE4, 0x8C, + 0xFF, 0xFB, 0xE7, 0x91, 0xFF, 0xFC, 0xEA, 0x95, 0xFF, 0xFC, 0xED, 0x9A, 0xFF, 0xFB, 0xEA, 0x9E, 0xFF, 0xFA, 0xE7, + 0xA3, 0xFF, 0xFA, 0xCC, 0x5E, 0xFF, 0xF5, 0xB6, 0x2C, 0xFF, 0xF9, 0xB8, 0x24, 0xFF, 0xF5, 0xB1, 0x14, 0xFF, 0xFF, + 0xFB, 0xFF, 0xFF, 0xEC, 0xFF, 0xFD, 0xFF, 0xED, 0xFF, 0xFF, 0xFF, 0xED, 0xFF, 0xFF, 0xFF, 0xEC, 0xFE, 0xFF, 0xFF, + 0xEA, 0xFD, 0xFD, 0xFF, 0xE3, 0xFD, 0xFD, 0xFF, 0xDC, 0xFD, 0xFD, 0xFF, 0xD5, 0xFD, 0xFD, 0xFF, 0xCE, 0xFD, 0xFD, + 0xFF, 0xC1, 0xFC, 0xFC, 0xFF, 0xB4, 0xFB, 0xFB, 0xFF, 0x8D, 0xFA, 0xF5, 0xFF, 0x89, 0xFC, 0xF8, 0xFF, 0xCB, 0xFA, + 0xF8, 0xFF, 0xF1, 0xFE, 0xF7, 0xFF, 0xBD, 0xFF, 0xF9, 0xFF, 0xC2, 0xF9, 0xFA, 0xFF, 0xAC, 0xF8, 0xFB, 0xFF, 0x96, + 0xF7, 0xFC, 0xFF, 0x91, 0xF4, 0xF9, 0xFF, 0x8C, 0xF0, 0xF7, 0xFF, 0xA8, 0xE4, 0xFF, 0xFF, 0xF6, 0x95, 0x00, 0xFF, + 0xF6, 0x99, 0x07, 0xFF, 0xF7, 0x9D, 0x15, 0xFF, 0xF3, 0x9D, 0x15, 0xFF, 0xF0, 0x9C, 0x15, 0xFF, 0xF2, 0x9A, 0x15, + 0xFF, 0xF4, 0x98, 0x15, 0xFF, 0xF2, 0x97, 0x14, 0xFF, 0xF1, 0x95, 0x12, 0xFF, 0xF1, 0x95, 0x12, 0xFF, 0xFA, 0xD9, + 0x6A, 0xFF, 0xF0, 0xCE, 0x60, 0xFF, 0xF6, 0xD2, 0x66, 0xFF, 0xFD, 0xDD, 0x5C, 0xFF, 0xC0, 0x6C, 0x1E, 0xFF, 0xBE, + 0x63, 0x0E, 0xFF, 0xCD, 0x69, 0x0E, 0xFF, 0xD0, 0x78, 0x13, 0xFF, 0xD4, 0x87, 0x18, 0xFF, 0xDE, 0x96, 0x19, 0xFF, + 0xE9, 0xA6, 0x1A, 0xFF, 0xE3, 0xA8, 0x13, 0xFF, 0xEE, 0xBA, 0x1D, 0xFF, 0xEA, 0xBD, 0x0C, 0xFF, 0xF6, 0xC5, 0x22, + 0xFF, 0xEC, 0xC5, 0x13, 0xFF, 0xF2, 0xCA, 0x2A, 0xFF, 0xF3, 0xCF, 0x40, 0xFF, 0xF4, 0xD3, 0x56, 0xFF, 0xF5, 0xD6, + 0x65, 0xFF, 0xF7, 0xD9, 0x74, 0xFF, 0xFA, 0xDF, 0x7B, 0xFF, 0xFE, 0xE4, 0x82, 0xFF, 0xFD, 0xE6, 0x86, 0xFF, 0xFD, + 0xE7, 0x89, 0xFF, 0xFB, 0xE4, 0x8D, 0xFF, 0xF9, 0xE2, 0x92, 0xFF, 0xFC, 0xBB, 0x33, 0xFF, 0xF6, 0xB9, 0x31, 0xFF, + 0xFD, 0xBA, 0x31, 0xFF, 0xF7, 0xC4, 0x57, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xE6, 0xFF, 0xFD, 0xFF, 0xE6, 0xFF, 0xFF, + 0xFF, 0xE6, 0xFF, 0xFF, 0xFF, 0xE6, 0xFE, 0xFF, 0xFF, 0xE5, 0xFC, 0xFD, 0xFF, 0xDD, 0xFC, 0xFD, 0xFF, 0xD5, 0xFC, + 0xFD, 0xFF, 0xCD, 0xFD, 0xFD, 0xFF, 0xC5, 0xFD, 0xFD, 0xFF, 0xBA, 0xFA, 0xFC, 0xFF, 0xAF, 0xF7, 0xFB, 0xFF, 0x9E, + 0xF9, 0xFE, 0xFF, 0x8D, 0xFB, 0xFF, 0xFF, 0x77, 0xFE, 0xFA, 0xFF, 0x7D, 0xFB, 0xF4, 0xFF, 0xD2, 0xF7, 0xF8, 0xFF, + 0xEE, 0xFF, 0xFD, 0xFF, 0xDF, 0xFD, 0xFE, 0xFF, 0xD0, 0xFB, 0xFE, 0xFF, 0xCD, 0xFA, 0xFD, 0xFF, 0xC9, 0xF9, 0xFC, + 0xFF, 0xCD, 0xD2, 0xA6, 0xFF, 0xEA, 0x98, 0x02, 0xFF, 0xEC, 0xA0, 0x1E, 0xFF, 0xF9, 0x9E, 0x13, 0xFF, 0xF6, 0x9E, + 0x15, 0xFF, 0xF2, 0x9D, 0x16, 0xFF, 0xF2, 0x9B, 0x15, 0xFF, 0xF3, 0x99, 0x14, 0xFF, 0xF2, 0x98, 0x13, 0xFF, 0xF1, + 0x97, 0x12, 0xFF, 0xF1, 0x97, 0x12, 0xFF, 0xF3, 0xD4, 0x55, 0xFF, 0xF0, 0xD1, 0x5B, 0xFF, 0xF6, 0xD6, 0x69, 0xFF, + 0xFF, 0xE2, 0x6D, 0xFF, 0xA7, 0x4F, 0x0B, 0xFF, 0xBE, 0x60, 0x11, 0xFF, 0xCD, 0x6A, 0x0F, 0xFF, 0xD5, 0x83, 0x1F, + 0xFF, 0xDC, 0x89, 0x1E, 0xFF, 0xDD, 0x8B, 0x0F, 0xFF, 0xE0, 0x9B, 0x1A, 0xFF, 0xF3, 0xB0, 0x22, 0xFF, 0xE0, 0xAA, + 0x1D, 0xFF, 0xDF, 0xAE, 0x13, 0xFF, 0xEE, 0xBC, 0x25, 0xFF, 0xE6, 0xB9, 0x14, 0xFF, 0xEF, 0xC1, 0x1F, 0xFF, 0xEF, + 0xC7, 0x25, 0xFF, 0xEE, 0xCD, 0x2B, 0xFF, 0xF0, 0xCD, 0x3C, 0xFF, 0xF3, 0xCE, 0x4E, 0xFF, 0xF8, 0xD6, 0x5B, 0xFF, + 0xFE, 0xDE, 0x68, 0xFF, 0xFC, 0xDD, 0x6D, 0xFF, 0xFA, 0xDC, 0x73, 0xFF, 0xF5, 0xDD, 0x75, 0xFF, 0xF6, 0xD3, 0x70, + 0xFF, 0xFB, 0xBA, 0x30, 0xFF, 0xF5, 0xB8, 0x33, 0xFF, 0xFE, 0xB5, 0x24, 0xFF, 0xE4, 0xDE, 0xA3, 0xFF, 0xDC, 0xFF, + 0xF9, 0xFF, 0xDC, 0xFD, 0xFD, 0xFF, 0xDC, 0xFE, 0xFE, 0xFF, 0xDB, 0xFF, 0xFF, 0xFF, 0xDA, 0xFE, 0xFE, 0xFF, 0xD9, + 0xFD, 0xFC, 0xFF, 0xD2, 0xFD, 0xFC, 0xFF, 0xCA, 0xFD, 0xFC, 0xFF, 0xC3, 0xFD, 0xFD, 0xFF, 0xBB, 0xFD, 0xFD, 0xFF, + 0xAF, 0xFB, 0xFC, 0xFF, 0xA2, 0xFA, 0xFC, 0xFF, 0x92, 0xFA, 0xFD, 0xFF, 0x83, 0xFB, 0xFE, 0xFF, 0x6A, 0xFD, 0xFB, + 0xFF, 0x60, 0xFC, 0xF8, 0xFF, 0x5D, 0xF8, 0xFA, 0xFF, 0x4C, 0xF7, 0xFC, 0xFF, 0x76, 0xF4, 0xFD, 0xFF, 0xA0, 0xF2, + 0xFE, 0xFF, 0x87, 0xEC, 0xF5, 0xFF, 0x5F, 0xE3, 0xF7, 0xFF, 0xB4, 0xBB, 0x50, 0xFF, 0xFE, 0x99, 0x0C, 0xFF, 0xF7, + 0x9E, 0x1A, 0xFF, 0xF6, 0x9D, 0x15, 0xFF, 0xF4, 0x9D, 0x15, 0xFF, 0xF2, 0x9C, 0x15, 0xFF, 0xF2, 0x9B, 0x14, 0xFF, + 0xF1, 0x99, 0x12, 0xFF, 0xF1, 0x99, 0x12, 0xFF, 0xF1, 0x99, 0x12, 0xFF, 0xF1, 0x99, 0x12, 0xFF, 0xFD, 0xD3, 0x66, + 0xFF, 0xF9, 0xD6, 0x69, 0xFF, 0xF5, 0xD9, 0x6B, 0xFF, 0xDC, 0xB6, 0x4E, 0xFF, 0xAE, 0x52, 0x18, 0xFF, 0xC6, 0x66, + 0x1C, 0xFF, 0xBD, 0x5A, 0x00, 0xFF, 0xCA, 0x7D, 0x1A, 0xFF, 0xD4, 0x7B, 0x15, 0xFF, 0xDC, 0x81, 0x04, 0xFF, 0xE7, + 0xA0, 0x2A, 0xFF, 0xD3, 0x88, 0x00, 0xFF, 0xE2, 0xAB, 0x2D, 0xFF, 0xDC, 0xA7, 0x23, 0xFF, 0xE6, 0xB3, 0x29, 0xFF, + 0xE0, 0xAD, 0x16, 0xFF, 0xEB, 0xB7, 0x14, 0xFF, 0xEA, 0xB9, 0x15, 0xFF, 0xE9, 0xBA, 0x16, 0xFF, 0xEC, 0xBE, 0x1F, + 0xFF, 0xEE, 0xC2, 0x28, 0xFF, 0xF6, 0xCD, 0x3B, 0xFF, 0xFE, 0xD8, 0x4E, 0xFF, 0xFB, 0xD5, 0x55, 0xFF, 0xF7, 0xD1, + 0x5D, 0xFF, 0xEF, 0xD6, 0x5D, 0xFF, 0xF3, 0xC4, 0x4E, 0xFF, 0xFA, 0xB9, 0x2E, 0xFF, 0xF4, 0xB8, 0x35, 0xFF, 0xFF, + 0xB1, 0x17, 0xFF, 0xD1, 0xF7, 0xF0, 0xFF, 0xDA, 0xFF, 0xFE, 0xFF, 0xD2, 0xFC, 0xFC, 0xFF, 0xD1, 0xFD, 0xFD, 0xFF, + 0xD0, 0xFE, 0xFD, 0xFF, 0xCF, 0xFD, 0xFC, 0xFF, 0xCD, 0xFD, 0xFB, 0xFF, 0xC6, 0xFD, 0xFC, 0xFF, 0xBF, 0xFD, 0xFC, + 0xFF, 0xB9, 0xFD, 0xFD, 0xFF, 0xB2, 0xFC, 0xFD, 0xFF, 0xA3, 0xFC, 0xFD, 0xFF, 0x95, 0xFC, 0xFD, 0xFF, 0x87, 0xFC, + 0xFC, 0xFF, 0x78, 0xFB, 0xFC, 0xFF, 0x6C, 0xFA, 0xFD, 0xFF, 0x5F, 0xF8, 0xFD, 0xFF, 0x45, 0xF6, 0xF9, 0xFF, 0x47, + 0xEF, 0xF5, 0xFF, 0x37, 0xE9, 0xF2, 0xFF, 0x28, 0xE4, 0xEE, 0xFF, 0x24, 0xE3, 0xED, 0xFF, 0x05, 0xDD, 0xFF, 0xFF, + 0xFF, 0x99, 0x03, 0xFF, 0xF5, 0xA0, 0x16, 0xFF, 0xF4, 0x9E, 0x16, 0xFF, 0xF3, 0x9C, 0x16, 0xFF, 0xF2, 0x9C, 0x15, + 0xFF, 0xF2, 0x9B, 0x14, 0xFF, 0xF1, 0x9A, 0x12, 0xFF, 0xEF, 0x99, 0x10, 0xFF, 0xF0, 0x9A, 0x11, 0xFF, 0xF1, 0x9B, + 0x12, 0xFF, 0xF1, 0x9B, 0x12, 0xFF, 0xFB, 0xD5, 0x65, 0xFF, 0xFC, 0xD4, 0x70, 0xFF, 0xFF, 0xE2, 0x77, 0xFF, 0xC7, + 0x86, 0x3B, 0xFF, 0xBA, 0x5F, 0x23, 0xFF, 0xBA, 0x6A, 0x1E, 0xFF, 0xD0, 0x7A, 0x21, 0xFF, 0xD7, 0x87, 0x27, 0xFF, + 0xD6, 0x8C, 0x24, 0xFF, 0xD3, 0x8D, 0x1D, 0xFF, 0xD0, 0x88, 0x21, 0xFF, 0xEA, 0xA0, 0x2B, 0xFF, 0xD5, 0x95, 0x21, + 0xFF, 0xEE, 0xA9, 0x30, 0xFF, 0xDA, 0xA0, 0x20, 0xFF, 0xDD, 0xA1, 0x16, 0xFF, 0xDF, 0xA1, 0x0D, 0xFF, 0xE2, 0xAB, + 0x19, 0xFF, 0xEB, 0xB1, 0x12, 0xFF, 0xED, 0xB8, 0x0F, 0xFF, 0xEE, 0xBF, 0x0C, 0xFF, 0xEF, 0xC1, 0x1C, 0xFF, 0xF0, + 0xC3, 0x2C, 0xFF, 0xF1, 0xC4, 0x36, 0xFF, 0xF3, 0xC5, 0x40, 0xFF, 0xF1, 0xC9, 0x46, 0xFF, 0xF6, 0xC2, 0x45, 0xFF, + 0xF9, 0xBA, 0x31, 0xFF, 0xF6, 0xB7, 0x30, 0xFF, 0xF4, 0xC1, 0x4B, 0xFF, 0xC0, 0xFA, 0xF5, 0xFF, 0xC6, 0xFF, 0xFD, + 0xFF, 0xC4, 0xFC, 0xFC, 0xFF, 0xC4, 0xFC, 0xFD, 0xFF, 0xC3, 0xFD, 0xFD, 0xFF, 0xC2, 0xFD, 0xFC, 0xFF, 0xC1, 0xFC, + 0xFB, 0xFF, 0xB5, 0xF8, 0xF8, 0xFF, 0xB2, 0xFD, 0xFC, 0xFF, 0xAA, 0xFC, 0xFC, 0xFF, 0xA3, 0xFC, 0xFC, 0xFF, 0x95, + 0xFB, 0xFC, 0xFF, 0x88, 0xFB, 0xFB, 0xFF, 0x7A, 0xFB, 0xFB, 0xFF, 0x6D, 0xFA, 0xFB, 0xFF, 0x61, 0xF8, 0xFB, 0xFF, + 0x56, 0xF6, 0xFC, 0xFF, 0x44, 0xF2, 0xF8, 0xFF, 0x40, 0xEA, 0xF4, 0xFF, 0x31, 0xE5, 0xEF, 0xFF, 0x23, 0xDF, 0xEA, + 0xFF, 0x1C, 0xE0, 0xFA, 0xFF, 0x44, 0xD1, 0xC5, 0xFF, 0xFE, 0xA1, 0x0A, 0xFF, 0xF9, 0x9F, 0x15, 0xFF, 0xF5, 0x9F, + 0x17, 0xFF, 0xF2, 0x9F, 0x18, 0xFF, 0xF2, 0x9E, 0x16, 0xFF, 0xF2, 0x9D, 0x15, 0xFF, 0xF5, 0x9F, 0x16, 0xFF, 0xF8, + 0xA0, 0x18, 0xFF, 0xF5, 0x9D, 0x15, 0xFF, 0xF2, 0x9A, 0x12, 0xFF, 0xF2, 0x9A, 0x12, 0xFF, 0xF9, 0xD7, 0x64, 0xFF, + 0xF6, 0xD1, 0x64, 0xFF, 0xFF, 0xE6, 0x5D, 0xFF, 0x9A, 0x43, 0x03, 0xFF, 0xA5, 0x4B, 0x0D, 0xFF, 0xCC, 0x7B, 0x30, + 0xFF, 0xC0, 0x54, 0x04, 0xFF, 0xC9, 0x53, 0x00, 0xFF, 0xC5, 0x67, 0x03, 0xFF, 0xC9, 0x87, 0x25, 0xFF, 0xCA, 0x80, + 0x28, 0xFF, 0xD0, 0x88, 0x27, 0xFF, 0xD7, 0x90, 0x26, 0xFF, 0xC9, 0x74, 0x06, 0xFF, 0xCF, 0x8D, 0x17, 0xFF, 0xE1, + 0x9C, 0x1E, 0xFF, 0xE3, 0x9B, 0x16, 0xFF, 0xDA, 0x9E, 0x1E, 0xFF, 0xDD, 0x97, 0x00, 0xFF, 0xE6, 0xA4, 0x03, 0xFF, + 0xEE, 0xB1, 0x07, 0xFF, 0xE8, 0xB0, 0x08, 0xFF, 0xE2, 0xAE, 0x09, 0xFF, 0xE8, 0xB4, 0x16, 0xFF, 0xEF, 0xB9, 0x23, + 0xFF, 0xF4, 0xBD, 0x30, 0xFF, 0xF9, 0xC1, 0x3C, 0xFF, 0xF9, 0xBB, 0x34, 0xFF, 0xF9, 0xB6, 0x2C, 0xFF, 0xE8, 0xD2, + 0x80, 0xFF, 0xAE, 0xFD, 0xFA, 0xFF, 0xB2, 0xFC, 0xFB, 0xFF, 0xB6, 0xFB, 0xFC, 0xFF, 0xB6, 0xFC, 0xFC, 0xFF, 0xB6, + 0xFC, 0xFD, 0xFF, 0xB5, 0xFC, 0xFC, 0xFF, 0xB4, 0xFC, 0xFB, 0xFF, 0xA4, 0xF4, 0xF3, 0xFF, 0xA5, 0xFC, 0xFC, 0xFF, + 0x9C, 0xFC, 0xFC, 0xFF, 0x93, 0xFB, 0xFC, 0xFF, 0x87, 0xFB, 0xFB, 0xFF, 0x7A, 0xFA, 0xFA, 0xFF, 0x6D, 0xFA, 0xFA, + 0xFF, 0x61, 0xF9, 0xF9, 0xFF, 0x57, 0xF7, 0xFA, 0xFF, 0x4E, 0xF4, 0xFA, 0xFF, 0x44, 0xED, 0xF6, 0xFF, 0x3A, 0xE6, + 0xF3, 0xFF, 0x2C, 0xE1, 0xED, 0xFF, 0x1E, 0xDB, 0xE7, 0xFF, 0x19, 0xD1, 0xFF, 0xFF, 0x8F, 0xB0, 0x77, 0xFF, 0xFD, + 0xA0, 0x09, 0xFF, 0xFD, 0x9D, 0x14, 0xFF, 0xF7, 0x9F, 0x17, 0xFF, 0xF2, 0xA2, 0x1A, 0xFF, 0xF2, 0xA0, 0x18, 0xFF, + 0xF2, 0x9E, 0x16, 0xFF, 0xF1, 0x9B, 0x13, 0xFF, 0xF0, 0x98, 0x10, 0xFF, 0xF1, 0x99, 0x11, 0xFF, 0xF2, 0x9A, 0x12, + 0xFF, 0xF2, 0x9A, 0x12, 0xFF, 0xF7, 0xD4, 0x5F, 0xFF, 0xFC, 0xDC, 0x67, 0xFF, 0xF0, 0xC1, 0x4F, 0xFF, 0x8A, 0x2B, + 0x00, 0xFF, 0xBF, 0x6A, 0x2D, 0xFF, 0xAC, 0x47, 0x05, 0xFF, 0xB9, 0x43, 0x00, 0xFF, 0xC4, 0x85, 0x35, 0xFF, 0xBB, + 0x4D, 0x06, 0xFF, 0xC3, 0x61, 0x13, 0xFF, 0xCA, 0x70, 0x2C, 0xFF, 0xB3, 0x5A, 0x0F, 0xFF, 0xCC, 0x74, 0x21, 0xFF, + 0xC2, 0x69, 0x11, 0xFF, 0xC2, 0x78, 0x18, 0xFF, 0xD0, 0x80, 0x1C, 0xFF, 0xD6, 0x7F, 0x18, 0xFF, 0xD3, 0x86, 0x1A, + 0xFF, 0xDD, 0x8F, 0x10, 0xFF, 0xDA, 0x8C, 0x02, 0xFF, 0xE6, 0x99, 0x04, 0xFF, 0xE1, 0x9B, 0x04, 0xFF, 0xDC, 0x9D, + 0x04, 0xFF, 0xE1, 0xA6, 0x05, 0xFF, 0xDD, 0xA6, 0x00, 0xFF, 0xEE, 0xB6, 0x1F, 0xFF, 0xF6, 0xBD, 0x39, 0xFF, 0xF6, + 0xBB, 0x38, 0xFF, 0xFC, 0xB5, 0x24, 0xFF, 0xB8, 0xE8, 0xBF, 0xFF, 0xA2, 0xFE, 0xFA, 0xFF, 0xA5, 0xFC, 0xFB, 0xFF, + 0xA8, 0xFA, 0xFB, 0xFF, 0xA7, 0xFB, 0xFC, 0xFF, 0xA6, 0xFC, 0xFC, 0xFF, 0xA2, 0xFB, 0xFA, 0xFF, 0x9F, 0xFA, 0xF8, + 0xFF, 0x94, 0xF7, 0xF5, 0xFF, 0x92, 0xFB, 0xFA, 0xFF, 0x8B, 0xFB, 0xFA, 0xFF, 0x84, 0xFB, 0xFB, 0xFF, 0x78, 0xFA, + 0xFA, 0xFF, 0x6D, 0xF9, 0xF9, 0xFF, 0x61, 0xF9, 0xF9, 0xFF, 0x55, 0xF8, 0xF8, 0xFF, 0x4B, 0xF6, 0xF8, 0xFF, 0x41, + 0xF3, 0xF9, 0xFF, 0x39, 0xEC, 0xF5, 0xFF, 0x30, 0xE4, 0xF1, 0xFF, 0x28, 0xDD, 0xEE, 0xFF, 0x1F, 0xD6, 0xEB, 0xFF, + 0x00, 0xD9, 0xEE, 0xFF, 0xE4, 0xA6, 0x32, 0xFF, 0xFF, 0xA4, 0x18, 0xFF, 0xF3, 0xA4, 0x28, 0xFF, 0xF4, 0xA2, 0x20, + 0xFF, 0xF4, 0xA0, 0x18, 0xFF, 0xF4, 0x9E, 0x16, 0xFF, 0xF3, 0x9D, 0x15, 0xFF, 0xF2, 0x9B, 0x13, 0xFF, 0xF2, 0x99, + 0x11, 0xFF, 0xF2, 0x99, 0x11, 0xFF, 0xF3, 0x9A, 0x12, 0xFF, 0xF3, 0x9A, 0x12, 0xFF, 0xF5, 0xD1, 0x5B, 0xFF, 0xFA, + 0xDF, 0x62, 0xFF, 0xCC, 0x8C, 0x30, 0xFF, 0x91, 0x2C, 0x05, 0xFF, 0x9A, 0x49, 0x0E, 0xFF, 0x9E, 0x36, 0x00, 0xFF, + 0x96, 0x38, 0x00, 0xFF, 0xB6, 0x5E, 0x14, 0xFF, 0xD9, 0xAA, 0x53, 0xFF, 0xE2, 0xA6, 0x30, 0xFF, 0xEE, 0xBB, 0x44, + 0xFF, 0xFF, 0xDD, 0x6D, 0xFF, 0xF9, 0xDE, 0x76, 0xFF, 0xF9, 0xD9, 0x6C, 0xFF, 0xF8, 0xD4, 0x63, 0xFF, 0xF3, 0xC4, + 0x54, 0xFF, 0xED, 0xB4, 0x44, 0xFF, 0xD5, 0x8E, 0x23, 0xFF, 0xCE, 0x77, 0x11, 0xFF, 0xC6, 0x6C, 0x00, 0xFF, 0xDE, + 0x81, 0x02, 0xFF, 0xDA, 0x87, 0x00, 0xFF, 0xD6, 0x8D, 0x00, 0xFF, 0xE1, 0x9B, 0x06, 0xFF, 0xDC, 0x98, 0x00, 0xFF, + 0xF0, 0xB1, 0x22, 0xFF, 0xF4, 0xB9, 0x35, 0xFF, 0xF3, 0xBC, 0x3C, 0xFF, 0xFF, 0xB4, 0x1B, 0xFF, 0x89, 0xFD, 0xFE, + 0xFF, 0x95, 0xFF, 0xFA, 0xFF, 0x97, 0xFC, 0xFA, 0xFF, 0x99, 0xF8, 0xFB, 0xFF, 0x97, 0xFB, 0xFB, 0xFF, 0x95, 0xFD, + 0xFC, 0xFF, 0x8F, 0xFB, 0xF9, 0xFF, 0x89, 0xF9, 0xF6, 0xFF, 0x84, 0xF9, 0xF7, 0xFF, 0x7F, 0xF9, 0xF8, 0xFF, 0x7A, + 0xFA, 0xF9, 0xFF, 0x75, 0xFA, 0xFA, 0xFF, 0x6A, 0xF9, 0xF9, 0xFF, 0x5F, 0xF9, 0xF8, 0xFF, 0x54, 0xF8, 0xF7, 0xFF, + 0x49, 0xF7, 0xF6, 0xFF, 0x3F, 0xF5, 0xF7, 0xFF, 0x35, 0xF2, 0xF7, 0xFF, 0x2E, 0xEB, 0xF3, 0xFF, 0x27, 0xE3, 0xF0, + 0xFF, 0x24, 0xDA, 0xF0, 0xFF, 0x21, 0xD1, 0xF0, 0xFF, 0x23, 0xC9, 0xE8, 0xFF, 0xFF, 0x9B, 0x03, 0xFF, 0xF6, 0xA3, + 0x20, 0xFF, 0xF6, 0xA1, 0x16, 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9D, 0x16, 0xFF, 0xF6, 0x9C, 0x15, 0xFF, 0xF5, + 0x9B, 0x14, 0xFF, 0xF4, 0x9A, 0x13, 0xFF, 0xF3, 0x99, 0x12, 0xFF, 0xF3, 0x99, 0x12, 0xFF, 0xF3, 0x99, 0x12, 0xFF, + 0xF3, 0x99, 0x12, 0xFF, 0xFE, 0xE2, 0x5A, 0xFF, 0xFF, 0xD7, 0x64, 0xFF, 0x97, 0x46, 0x0C, 0xFF, 0x82, 0x25, 0x00, + 0xFF, 0xB7, 0x6A, 0x1D, 0xFF, 0xDE, 0xA2, 0x39, 0xFF, 0xFF, 0xE5, 0x5E, 0xFF, 0xFD, 0xD8, 0x51, 0xFF, 0xF5, 0xD6, + 0x4C, 0xFF, 0xF4, 0xCC, 0x48, 0xFF, 0xF6, 0xCF, 0x5E, 0xFF, 0xFE, 0xD9, 0x67, 0xFF, 0xF7, 0xD3, 0x61, 0xFF, 0xF8, + 0xD1, 0x5A, 0xFF, 0xFE, 0xCB, 0x41, 0xFF, 0xFE, 0xCE, 0x53, 0xFF, 0xF5, 0xCF, 0x51, 0xFF, 0xF6, 0xCA, 0x49, 0xFF, + 0xFF, 0xCD, 0x49, 0xFF, 0xFF, 0xB9, 0x3F, 0xFF, 0xDA, 0x7E, 0x0E, 0xFF, 0xC2, 0x69, 0x00, 0xFF, 0xDA, 0x84, 0x05, + 0xFF, 0xD5, 0x84, 0x01, 0xFF, 0xD8, 0x8C, 0x05, 0xFF, 0xF8, 0xBE, 0x37, 0xFF, 0xF6, 0xBE, 0x3A, 0xFF, 0xFF, 0xBD, + 0x34, 0xFF, 0xE1, 0xC6, 0x61, 0xFF, 0x79, 0xF3, 0xFB, 0xFF, 0x82, 0xFA, 0xF7, 0xFF, 0x83, 0xF9, 0xF9, 0xFF, 0x83, + 0xF7, 0xFA, 0xFF, 0x7F, 0xF7, 0xF8, 0xFF, 0x7B, 0xF6, 0xF6, 0xFF, 0x79, 0xF8, 0xF7, 0xFF, 0x77, 0xFA, 0xF8, 0xFF, + 0x71, 0xF9, 0xF7, 0xFF, 0x6C, 0xF8, 0xF7, 0xFF, 0x6B, 0xFC, 0xFB, 0xFF, 0x63, 0xF8, 0xF8, 0xFF, 0x5A, 0xF7, 0xF8, + 0xFF, 0x52, 0xF7, 0xF7, 0xFF, 0x48, 0xF5, 0xF7, 0xFF, 0x3F, 0xF4, 0xF6, 0xFF, 0x37, 0xF2, 0xF5, 0xFF, 0x2F, 0xEF, + 0xF4, 0xFF, 0x27, 0xE6, 0xF1, 0xFF, 0x20, 0xDD, 0xEE, 0xFF, 0x1F, 0xD6, 0xEA, 0xFF, 0x10, 0xCC, 0xF1, 0xFF, 0x6C, + 0xB9, 0x9D, 0xFF, 0xFE, 0x9F, 0x0B, 0xFF, 0xF8, 0xA3, 0x1A, 0xFF, 0xF9, 0xA2, 0x16, 0xFF, 0xF8, 0xA0, 0x16, 0xFF, + 0xF7, 0x9E, 0x16, 0xFF, 0xF7, 0x9D, 0x15, 0xFF, 0xF6, 0x9B, 0x14, 0xFF, 0xF5, 0x9A, 0x14, 0xFF, 0xF4, 0x99, 0x13, + 0xFF, 0xF4, 0x99, 0x13, 0xFF, 0xF4, 0x99, 0x13, 0xFF, 0xF4, 0x99, 0x13, 0xFF, 0xF8, 0xD8, 0x60, 0xFF, 0xF7, 0xD8, + 0x5A, 0xFF, 0xD7, 0xAD, 0x4B, 0xFF, 0xFF, 0xDD, 0x68, 0xFF, 0xF7, 0xDC, 0x55, 0xFF, 0xFC, 0xD6, 0x55, 0xFF, 0xFF, + 0xCF, 0x54, 0xFF, 0xFF, 0xD5, 0x5C, 0xFF, 0xF1, 0xCA, 0x53, 0xFF, 0xF5, 0xCA, 0x4A, 0xFF, 0xF9, 0xC9, 0x42, 0xFF, + 0xF7, 0xC9, 0x47, 0xFF, 0xF5, 0xC8, 0x4B, 0xFF, 0xF0, 0xCF, 0x5C, 0xFF, 0xF8, 0xCC, 0x46, 0xFF, 0xFF, 0xCA, 0x55, + 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xFB, 0xC2, 0x43, 0xFF, 0xFC, 0xC1, 0x48, 0xFF, 0xF3, 0xBE, 0x3E, 0xFF, 0xFA, 0xCB, + 0x43, 0xFF, 0xFC, 0xB3, 0x37, 0xFF, 0xDD, 0x7B, 0x0B, 0xFF, 0xC8, 0x6D, 0x00, 0xFF, 0xD4, 0x7F, 0x0D, 0xFF, 0xFF, + 0xCC, 0x4D, 0xFF, 0xF9, 0xC2, 0x3E, 0xFF, 0xFF, 0xC1, 0x2D, 0xFF, 0xA7, 0xDE, 0xA7, 0xFF, 0x5B, 0xEB, 0xF7, 0xFF, + 0x6F, 0xF5, 0xF4, 0xFF, 0x6E, 0xF5, 0xF7, 0xFF, 0x6D, 0xF6, 0xF9, 0xFF, 0x67, 0xF3, 0xF5, 0xFF, 0x60, 0xF0, 0xF1, + 0xFF, 0x62, 0xF6, 0xF5, 0xFF, 0x65, 0xFC, 0xFA, 0xFF, 0x5E, 0xF9, 0xF8, 0xFF, 0x58, 0xF6, 0xF5, 0xFF, 0x5D, 0xFE, + 0xFE, 0xFF, 0x52, 0xF6, 0xF6, 0xFF, 0x4B, 0xF5, 0xF6, 0xFF, 0x44, 0xF5, 0xF7, 0xFF, 0x3D, 0xF3, 0xF6, 0xFF, 0x35, + 0xF1, 0xF5, 0xFF, 0x2F, 0xEE, 0xF3, 0xFF, 0x28, 0xEB, 0xF0, 0xFF, 0x20, 0xE1, 0xEE, 0xFF, 0x18, 0xD8, 0xEC, 0xFF, + 0x1A, 0xD2, 0xE4, 0xFF, 0x00, 0xC6, 0xF3, 0xFF, 0xB4, 0xA8, 0x51, 0xFF, 0xFA, 0xA3, 0x13, 0xFF, 0xFB, 0xA3, 0x15, + 0xFF, 0xFB, 0xA3, 0x17, 0xFF, 0xFA, 0xA0, 0x16, 0xFF, 0xF8, 0x9E, 0x16, 0xFF, 0xF7, 0x9D, 0x15, 0xFF, 0xF7, 0x9C, + 0x15, 0xFF, 0xF6, 0x9A, 0x14, 0xFF, 0xF6, 0x99, 0x14, 0xFF, 0xF6, 0x99, 0x14, 0xFF, 0xF6, 0x99, 0x14, 0xFF, 0xF6, + 0x99, 0x14, 0xFF, 0xF1, 0xCE, 0x58, 0xFF, 0xFD, 0xDC, 0x59, 0xFF, 0xF8, 0xD5, 0x55, 0xFF, 0xFF, 0xDD, 0x5D, 0xFF, + 0xF3, 0xCE, 0x4D, 0xFF, 0xF3, 0xCB, 0x4C, 0xFF, 0xF3, 0xC8, 0x4C, 0xFF, 0xFB, 0xD1, 0x56, 0xFF, 0xFC, 0xD3, 0x58, + 0xFF, 0xFB, 0xCE, 0x4F, 0xFF, 0xFA, 0xC9, 0x47, 0xFF, 0xF9, 0xC8, 0x48, 0xFF, 0xF8, 0xC7, 0x49, 0xFF, 0xF5, 0xCA, + 0x50, 0xFF, 0xF9, 0xC9, 0x44, 0xFF, 0xFD, 0xC8, 0x4B, 0xFF, 0xF9, 0xC5, 0x3E, 0xFF, 0xFA, 0xC3, 0x40, 0xFF, 0xFA, + 0xC2, 0x43, 0xFF, 0xF3, 0xBD, 0x3A, 0xFF, 0xF3, 0xBF, 0x3A, 0xFF, 0xFC, 0xC7, 0x3E, 0xFF, 0xFC, 0xC6, 0x3A, 0xFF, + 0xE2, 0xA1, 0x24, 0xFF, 0xD9, 0x8C, 0x1F, 0xFF, 0xF6, 0xB9, 0x36, 0xFF, 0xFA, 0xBB, 0x26, 0xFF, 0xF3, 0xBA, 0x29, + 0xFF, 0x56, 0xD7, 0xCD, 0xFF, 0x5A, 0xFA, 0xF9, 0xFF, 0x48, 0xDA, 0xD9, 0xFF, 0x58, 0xEC, 0xED, 0xFF, 0x5F, 0xF5, + 0xF9, 0xFF, 0x4D, 0xEF, 0xF1, 0xFF, 0x3A, 0xE9, 0xE9, 0xFF, 0x45, 0xEE, 0xED, 0xFF, 0x50, 0xF4, 0xF2, 0xFF, 0x4E, + 0xF3, 0xF9, 0xFF, 0x44, 0xF0, 0xED, 0xFF, 0x4B, 0xF8, 0xFE, 0xFF, 0x41, 0xF5, 0xF4, 0xFF, 0x3C, 0xF4, 0xF5, 0xFF, + 0x37, 0xF2, 0xF6, 0xFF, 0x31, 0xF0, 0xF5, 0xFF, 0x2A, 0xEF, 0xF4, 0xFF, 0x26, 0xEA, 0xF2, 0xFF, 0x22, 0xE6, 0xF0, + 0xFF, 0x1C, 0xDB, 0xEE, 0xFF, 0x17, 0xD0, 0xEC, 0xFF, 0x08, 0xCC, 0xF0, 0xFF, 0x08, 0xC4, 0xF5, 0xFF, 0xFF, 0xAD, + 0x0E, 0xFF, 0xF9, 0xA1, 0x16, 0xFF, 0xF8, 0xA1, 0x17, 0xFF, 0xF8, 0xA1, 0x18, 0xFF, 0xF8, 0xA0, 0x17, 0xFF, 0xF8, + 0x9E, 0x16, 0xFF, 0xF8, 0x9D, 0x16, 0xFF, 0xF8, 0x9C, 0x15, 0xFF, 0xF7, 0x9A, 0x15, 0xFF, 0xF7, 0x99, 0x14, 0xFF, + 0xF7, 0x99, 0x14, 0xFF, 0xF7, 0x99, 0x14, 0xFF, 0xF7, 0x99, 0x14, 0xFF, 0xFB, 0xD5, 0x60, 0xFF, 0xFA, 0xD3, 0x5A, + 0xFF, 0xFA, 0xD1, 0x55, 0xFF, 0xFC, 0xD0, 0x55, 0xFF, 0xFE, 0xCF, 0x54, 0xFF, 0xFA, 0xD0, 0x54, 0xFF, 0xF6, 0xD1, + 0x53, 0xFF, 0xF6, 0xCE, 0x50, 0xFF, 0xF7, 0xCB, 0x4E, 0xFF, 0xF9, 0xCA, 0x4C, 0xFF, 0xFA, 0xCA, 0x4B, 0xFF, 0xFB, + 0xC8, 0x49, 0xFF, 0xFB, 0xC6, 0x47, 0xFF, 0xFB, 0xC6, 0x45, 0xFF, 0xFA, 0xC6, 0x43, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, + 0xF8, 0xC6, 0x3F, 0xFF, 0xF8, 0xC4, 0x3E, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xFB, 0xC3, 0x3F, 0xFF, 0xFD, 0xC3, 0x40, + 0xFF, 0xF2, 0xBA, 0x38, 0xFF, 0xF7, 0xC0, 0x3F, 0xFF, 0xFA, 0xC2, 0x3D, 0xFF, 0xFD, 0xC5, 0x3A, 0xFF, 0xF6, 0xC1, + 0x37, 0xFF, 0xEF, 0xBD, 0x34, 0xFF, 0xEF, 0xBB, 0x2D, 0xFF, 0x21, 0xD6, 0xDD, 0xFF, 0x37, 0xDC, 0xBF, 0xFF, 0x41, + 0xE0, 0xDD, 0xFF, 0x49, 0xEA, 0xEB, 0xFF, 0x41, 0xE3, 0xEA, 0xFF, 0x41, 0xE8, 0xED, 0xFF, 0x41, 0xED, 0xF1, 0xFF, + 0x3F, 0xEC, 0xED, 0xFF, 0x3C, 0xEB, 0xEA, 0xFF, 0x3E, 0xEE, 0xFA, 0xFF, 0x31, 0xEB, 0xE5, 0xFF, 0x39, 0xF2, 0xFE, + 0xFF, 0x31, 0xF4, 0xF1, 0xFF, 0x2D, 0xF2, 0xF3, 0xFF, 0x29, 0xF0, 0xF5, 0xFF, 0x25, 0xEE, 0xF4, 0xFF, 0x20, 0xEC, + 0xF4, 0xFF, 0x1E, 0xE6, 0xF1, 0xFF, 0x1C, 0xE1, 0xEF, 0xFF, 0x19, 0xD5, 0xED, 0xFF, 0x16, 0xC9, 0xEB, 0xFF, 0x0B, + 0xC3, 0xDE, 0xFF, 0x39, 0xBE, 0xBA, 0xFF, 0xF8, 0x98, 0x07, 0xFF, 0xF8, 0x9F, 0x19, 0xFF, 0xF6, 0x9F, 0x19, 0xFF, + 0xF5, 0x9F, 0x19, 0xFF, 0xF7, 0x9F, 0x18, 0xFF, 0xF9, 0x9F, 0x16, 0xFF, 0xF9, 0x9D, 0x16, 0xFF, 0xF9, 0x9C, 0x16, + 0xFF, 0xF9, 0x9A, 0x16, 0xFF, 0xF8, 0x99, 0x15, 0xFF, 0xF8, 0x99, 0x15, 0xFF, 0xF8, 0x99, 0x15, 0xFF, 0xF8, 0x99, + 0x15, 0xFF, 0xF8, 0xD4, 0x5C, 0xFF, 0xF8, 0xD4, 0x58, 0xFF, 0xF8, 0xD3, 0x54, 0xFF, 0xF9, 0xD1, 0x56, 0xFF, 0xFA, + 0xD0, 0x57, 0xFF, 0xF8, 0xD0, 0x55, 0xFF, 0xF5, 0xD0, 0x53, 0xFF, 0xF7, 0xCE, 0x50, 0xFF, 0xF9, 0xCC, 0x4D, 0xFF, + 0xF9, 0xCB, 0x4C, 0xFF, 0xFA, 0xCA, 0x4A, 0xFF, 0xFB, 0xC8, 0x48, 0xFF, 0xFB, 0xC7, 0x46, 0xFF, 0xFA, 0xC6, 0x44, + 0xFF, 0xFA, 0xC6, 0x43, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, 0xC6, 0x3F, 0xFF, 0xF9, 0xC4, 0x3E, 0xFF, 0xF9, 0xC3, + 0x3D, 0xFF, 0xFA, 0xC2, 0x3E, 0xFF, 0xFB, 0xC1, 0x3E, 0xFF, 0xF5, 0xBD, 0x3A, 0xFF, 0xF7, 0xC1, 0x3D, 0xFF, 0xF8, + 0xC0, 0x3A, 0xFF, 0xF9, 0xC0, 0x37, 0xFF, 0xFF, 0xBD, 0x36, 0xFF, 0xFF, 0xBB, 0x35, 0xFF, 0x84, 0xBA, 0x66, 0xFF, + 0x18, 0xD2, 0xAF, 0xFF, 0x19, 0xD2, 0xB3, 0xFF, 0x39, 0xDA, 0xD2, 0xFF, 0x3D, 0xDC, 0xE1, 0xFF, 0x31, 0xD4, 0xD5, + 0xFF, 0x37, 0xDF, 0xE1, 0xFF, 0x3E, 0xE9, 0xEC, 0xFF, 0x35, 0xE6, 0xE1, 0xFF, 0x35, 0xE5, 0xE9, 0xFF, 0x34, 0xE5, + 0xF0, 0xFF, 0x2A, 0xE3, 0xE4, 0xFF, 0x2D, 0xE5, 0xF5, 0xFF, 0x28, 0xEB, 0xE8, 0xFF, 0x2A, 0xEE, 0xF0, 0xFF, 0x24, + 0xE8, 0xEF, 0xFF, 0x20, 0xE4, 0xEC, 0xFF, 0x1C, 0xDF, 0xE9, 0xFF, 0x1C, 0xDB, 0xEB, 0xFF, 0x1B, 0xD7, 0xED, 0xFF, + 0x18, 0xCE, 0xE9, 0xFF, 0x15, 0xC5, 0xE5, 0xFF, 0x03, 0xBF, 0xE7, 0xFF, 0x92, 0xB1, 0x6C, 0xFF, 0xFB, 0x9C, 0x10, + 0xFF, 0xF7, 0xA0, 0x17, 0xFF, 0xF5, 0xA0, 0x19, 0xFF, 0xF3, 0xA0, 0x1B, 0xFF, 0xF6, 0x9F, 0x19, 0xFF, 0xF9, 0x9F, + 0x16, 0xFF, 0xF8, 0x9E, 0x16, 0xFF, 0xF8, 0x9C, 0x15, 0xFF, 0xF8, 0x9B, 0x15, 0xFF, 0xF8, 0x99, 0x15, 0xFF, 0xF7, + 0x99, 0x14, 0xFF, 0xF7, 0x98, 0x14, 0xFF, 0xF7, 0x98, 0x14, 0xFF, 0xF6, 0xD3, 0x57, 0xFF, 0xF6, 0xD4, 0x55, 0xFF, + 0xF6, 0xD5, 0x53, 0xFF, 0xF7, 0xD2, 0x57, 0xFF, 0xF7, 0xD0, 0x5B, 0xFF, 0xF6, 0xD0, 0x57, 0xFF, 0xF5, 0xCF, 0x54, + 0xFF, 0xF7, 0xCE, 0x50, 0xFF, 0xFA, 0xCC, 0x4C, 0xFF, 0xFA, 0xCB, 0x4B, 0xFF, 0xFA, 0xCA, 0x49, 0xFF, 0xFA, 0xC8, + 0x47, 0xFF, 0xFB, 0xC7, 0x46, 0xFF, 0xFA, 0xC7, 0x44, 0xFF, 0xFA, 0xC6, 0x43, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, + 0xC5, 0x3F, 0xFF, 0xF9, 0xC4, 0x3E, 0xFF, 0xF9, 0xC2, 0x3D, 0xFF, 0xF9, 0xC1, 0x3C, 0xFF, 0xF9, 0xC0, 0x3B, 0xFF, + 0xF8, 0xC1, 0x3C, 0xFF, 0xF7, 0xC2, 0x3C, 0xFF, 0xF6, 0xBE, 0x38, 0xFF, 0xF5, 0xBB, 0x34, 0xFF, 0xFD, 0xBC, 0x35, + 0xFF, 0xFF, 0xBE, 0x36, 0xFF, 0xFB, 0xBB, 0x45, 0xFF, 0x2B, 0xC9, 0x82, 0xFF, 0x01, 0xBE, 0xA0, 0xFF, 0x20, 0xC4, + 0xB8, 0xFF, 0x31, 0xCF, 0xD8, 0xFF, 0x31, 0xD5, 0xD1, 0xFF, 0x2E, 0xD5, 0xD4, 0xFF, 0x2A, 0xD4, 0xD7, 0xFF, 0x24, + 0xD7, 0xCC, 0xFF, 0x2E, 0xDE, 0xE8, 0xFF, 0x29, 0xDD, 0xE6, 0xFF, 0x24, 0xDC, 0xE4, 0xFF, 0x22, 0xD9, 0xED, 0xFF, + 0x20, 0xE1, 0xDF, 0xFF, 0x27, 0xE9, 0xEC, 0xFF, 0x1E, 0xE0, 0xEA, 0xFF, 0x1B, 0xD9, 0xE3, 0xFF, 0x19, 0xD3, 0xDD, + 0xFF, 0x1A, 0xD0, 0xE4, 0xFF, 0x1B, 0xCD, 0xEB, 0xFF, 0x17, 0xC7, 0xE4, 0xFF, 0x14, 0xC2, 0xDE, 0xFF, 0x00, 0xBC, + 0xEF, 0xFF, 0xEB, 0xA4, 0x1D, 0xFF, 0xFF, 0xA0, 0x19, 0xFF, 0xF6, 0xA2, 0x15, 0xFF, 0xF3, 0xA2, 0x19, 0xFF, 0xF0, + 0xA1, 0x1D, 0xFF, 0xF4, 0xA0, 0x19, 0xFF, 0xF8, 0x9F, 0x16, 0xFF, 0xF8, 0x9E, 0x15, 0xFF, 0xF8, 0x9D, 0x15, 0xFF, + 0xF7, 0x9B, 0x14, 0xFF, 0xF7, 0x9A, 0x14, 0xFF, 0xF6, 0x99, 0x13, 0xFF, 0xF5, 0x98, 0x12, 0xFF, 0xF5, 0x98, 0x12, + 0xFF, 0xF8, 0xD5, 0x5E, 0xFF, 0xFC, 0xD5, 0x63, 0xFF, 0xFF, 0xD6, 0x68, 0xFF, 0xFB, 0xD2, 0x5E, 0xFF, 0xF8, 0xCF, + 0x55, 0xFF, 0xF7, 0xCF, 0x53, 0xFF, 0xF7, 0xCE, 0x50, 0xFF, 0xF9, 0xCD, 0x4D, 0xFF, 0xFA, 0xCC, 0x4B, 0xFF, 0xFA, + 0xCB, 0x49, 0xFF, 0xFA, 0xCA, 0x48, 0xFF, 0xFA, 0xC9, 0x47, 0xFF, 0xFA, 0xC8, 0x45, 0xFF, 0xFA, 0xC7, 0x44, 0xFF, + 0xF9, 0xC6, 0x42, 0xFF, 0xF9, 0xC5, 0x41, 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x3F, 0xFF, 0xF9, 0xC2, 0x3D, + 0xFF, 0xF9, 0xC1, 0x3C, 0xFF, 0xF8, 0xC0, 0x3B, 0xFF, 0xF8, 0xC0, 0x3B, 0xFF, 0xF8, 0xC1, 0x3A, 0xFF, 0xF7, 0xBF, + 0x38, 0xFF, 0xF6, 0xBD, 0x35, 0xFF, 0xFA, 0xBD, 0x34, 0xFF, 0xFE, 0xBD, 0x33, 0xFF, 0xF5, 0xC3, 0x22, 0xFF, 0xFB, + 0xBA, 0x26, 0xFF, 0xB1, 0xB0, 0x53, 0xFF, 0x06, 0xC5, 0x9A, 0xFF, 0x22, 0xD2, 0xC0, 0xFF, 0x36, 0xDD, 0xD3, 0xFF, + 0x12, 0xBA, 0xB3, 0xFF, 0x1E, 0xC7, 0xC3, 0xFF, 0x21, 0xCE, 0xC4, 0xFF, 0x2C, 0xD8, 0xD8, 0xFF, 0x2F, 0xDA, 0xDE, + 0xFF, 0x2A, 0xD5, 0xDC, 0xFF, 0x20, 0xD4, 0xE7, 0xFF, 0x1C, 0xD5, 0xD4, 0xFF, 0x28, 0xE4, 0xE8, 0xFF, 0x24, 0xE3, + 0xEB, 0xFF, 0x1F, 0xCD, 0xD1, 0xFF, 0x1C, 0xC5, 0xD2, 0xFF, 0x01, 0xC2, 0xDC, 0xFF, 0x11, 0xC3, 0xCF, 0xFF, 0x09, + 0xC1, 0xE2, 0xFF, 0x00, 0xBE, 0xE3, 0xFF, 0x6E, 0xBE, 0x83, 0xFF, 0xF6, 0x9F, 0x0C, 0xFF, 0xFD, 0x9F, 0x11, 0xFF, + 0xF6, 0xA1, 0x17, 0xFF, 0xF4, 0xA1, 0x19, 0xFF, 0xF3, 0xA1, 0x1A, 0xFF, 0xF5, 0xA0, 0x18, 0xFF, 0xF8, 0x9F, 0x15, + 0xFF, 0xF7, 0x9E, 0x15, 0xFF, 0xF7, 0x9D, 0x14, 0xFF, 0xF7, 0x9C, 0x14, 0xFF, 0xF7, 0x9B, 0x13, 0xFF, 0xF5, 0x99, + 0x11, 0xFF, 0xF4, 0x98, 0x10, 0xFF, 0xF4, 0x98, 0x10, 0xFF, 0xFB, 0xD6, 0x64, 0xFF, 0xF9, 0xD4, 0x5D, 0xFF, 0xF8, + 0xD2, 0x55, 0xFF, 0xF8, 0xD0, 0x53, 0xFF, 0xF8, 0xCE, 0x50, 0xFF, 0xF9, 0xCE, 0x4E, 0xFF, 0xFA, 0xCD, 0x4D, 0xFF, + 0xFA, 0xCC, 0x4B, 0xFF, 0xFB, 0xCC, 0x49, 0xFF, 0xFA, 0xCB, 0x48, 0xFF, 0xFA, 0xCA, 0x47, 0xFF, 0xFA, 0xC9, 0x46, + 0xFF, 0xFA, 0xC8, 0x45, 0xFF, 0xFA, 0xC7, 0x43, 0xFF, 0xF9, 0xC6, 0x42, 0xFF, 0xF9, 0xC5, 0x41, 0xFF, 0xF9, 0xC4, + 0x40, 0xFF, 0xF9, 0xC3, 0x3F, 0xFF, 0xF9, 0xC2, 0x3D, 0xFF, 0xF9, 0xC1, 0x3C, 0xFF, 0xF8, 0xC0, 0x3B, 0xFF, 0xF8, + 0xC0, 0x3A, 0xFF, 0xF8, 0xBF, 0x39, 0xFF, 0xF8, 0xBF, 0x38, 0xFF, 0xF8, 0xBF, 0x36, 0xFF, 0xF7, 0xBD, 0x34, 0xFF, + 0xF7, 0xBC, 0x31, 0xFF, 0xF8, 0xBB, 0x33, 0xFF, 0xF9, 0xBA, 0x35, 0xFF, 0xFF, 0xBC, 0x2C, 0xFF, 0xDE, 0xC2, 0x60, + 0xFF, 0x84, 0xCB, 0x93, 0xFF, 0x2A, 0xD4, 0xC5, 0xFF, 0x2E, 0xD7, 0xCA, 0xFF, 0x12, 0xBA, 0xB0, 0xFF, 0x16, 0xBE, + 0xB4, 0xFF, 0x1A, 0xC2, 0xB8, 0xFF, 0x25, 0xC8, 0xC6, 0xFF, 0x20, 0xBE, 0xC4, 0xFF, 0x16, 0xC8, 0xDA, 0xFF, 0x18, + 0xC8, 0xC9, 0xFF, 0x21, 0xD7, 0xDB, 0xFF, 0x1A, 0xD6, 0xDD, 0xFF, 0x0D, 0xBC, 0xB7, 0xFF, 0x03, 0xBD, 0xC7, 0xFF, + 0x00, 0xBF, 0xD0, 0xFF, 0x50, 0xC9, 0xAC, 0xFF, 0xB0, 0xB8, 0x6B, 0xFF, 0xFF, 0xA3, 0x04, 0xFF, 0xFA, 0xA3, 0x12, + 0xFF, 0xF4, 0xA4, 0x21, 0xFF, 0xF5, 0xA2, 0x1D, 0xFF, 0xF5, 0xA1, 0x19, 0xFF, 0xF6, 0xA0, 0x18, 0xFF, 0xF6, 0xA0, + 0x17, 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, 0xFF, 0xF7, 0x9D, 0x14, 0xFF, 0xF6, + 0x9C, 0x13, 0xFF, 0xF6, 0x9B, 0x12, 0xFF, 0xF4, 0x99, 0x10, 0xFF, 0xF2, 0x97, 0x0E, 0xFF, 0xF2, 0x97, 0x0E, 0xFF, + 0xF8, 0xD4, 0x5C, 0xFF, 0xF8, 0xD3, 0x57, 0xFF, 0xF8, 0xD1, 0x53, 0xFF, 0xF8, 0xD0, 0x51, 0xFF, 0xF9, 0xCE, 0x4F, + 0xFF, 0xF9, 0xCE, 0x4D, 0xFF, 0xF9, 0xCD, 0x4B, 0xFF, 0xFA, 0xCC, 0x4A, 0xFF, 0xFA, 0xCB, 0x48, 0xFF, 0xFA, 0xCA, + 0x47, 0xFF, 0xFA, 0xCA, 0x46, 0xFF, 0xF9, 0xC9, 0x45, 0xFF, 0xF9, 0xC8, 0x44, 0xFF, 0xF9, 0xC7, 0x43, 0xFF, 0xF9, + 0xC6, 0x42, 0xFF, 0xF9, 0xC5, 0x41, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3D, 0xFF, + 0xF9, 0xC1, 0x3C, 0xFF, 0xF9, 0xC0, 0x3A, 0xFF, 0xF8, 0xBF, 0x39, 0xFF, 0xF8, 0xBF, 0x38, 0xFF, 0xF8, 0xBF, 0x37, + 0xFF, 0xF8, 0xBE, 0x36, 0xFF, 0xF5, 0xBD, 0x35, 0xFF, 0xF3, 0xBB, 0x34, 0xFF, 0xF7, 0xB9, 0x34, 0xFF, 0xFA, 0xB7, + 0x34, 0xFF, 0xFF, 0xB5, 0x22, 0xFF, 0xFE, 0xB4, 0x2E, 0xFF, 0xE6, 0xB9, 0x4D, 0xFF, 0xCD, 0xBF, 0x6B, 0xFF, 0xC5, + 0xB1, 0x27, 0xFF, 0x7C, 0xBB, 0x6C, 0xFF, 0x48, 0xBD, 0x89, 0xFF, 0x15, 0xBE, 0xA6, 0xFF, 0x08, 0xBF, 0xB9, 0xFF, + 0x00, 0xBF, 0xCB, 0xFF, 0x3D, 0xC4, 0xDA, 0xFF, 0x20, 0xCA, 0xBA, 0xFF, 0x3E, 0xC6, 0xAD, 0xFF, 0x53, 0xBB, 0x99, + 0xFF, 0x8A, 0xAC, 0x59, 0xFF, 0xC3, 0xAA, 0x35, 0xFF, 0xFF, 0xB3, 0x03, 0xFF, 0xFF, 0xA6, 0x15, 0xFF, 0xFF, 0xA4, + 0x20, 0xFF, 0xFA, 0xA0, 0x19, 0xFF, 0xF9, 0xA2, 0x1B, 0xFF, 0xF8, 0xA4, 0x1C, 0xFF, 0xF7, 0xA2, 0x1B, 0xFF, 0xF6, + 0xA1, 0x19, 0xFF, 0xF6, 0xA0, 0x18, 0xFF, 0xF7, 0xA0, 0x17, 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, + 0xF7, 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x14, 0xFF, 0xF6, 0x9C, 0x13, 0xFF, 0xF6, 0x9B, 0x12, 0xFF, 0xF4, 0x9A, 0x10, + 0xFF, 0xF3, 0x98, 0x0F, 0xFF, 0xF3, 0x98, 0x0F, 0xFF, 0xF6, 0xD2, 0x53, 0xFF, 0xF7, 0xD1, 0x52, 0xFF, 0xF8, 0xD0, + 0x51, 0xFF, 0xF8, 0xCF, 0x4F, 0xFF, 0xF9, 0xCE, 0x4E, 0xFF, 0xF9, 0xCE, 0x4C, 0xFF, 0xF9, 0xCD, 0x4A, 0xFF, 0xF9, + 0xCC, 0x48, 0xFF, 0xF9, 0xCB, 0x46, 0xFF, 0xF9, 0xCA, 0x46, 0xFF, 0xF9, 0xC9, 0x45, 0xFF, 0xF9, 0xC8, 0x44, 0xFF, + 0xF9, 0xC8, 0x43, 0xFF, 0xF9, 0xC7, 0x42, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, 0xC5, 0x41, 0xFF, 0xF9, 0xC4, 0x40, + 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3D, 0xFF, 0xF9, 0xC1, 0x3B, 0xFF, 0xF9, 0xC0, 0x3A, 0xFF, 0xF9, 0xBF, + 0x38, 0xFF, 0xF8, 0xBF, 0x37, 0xFF, 0xF8, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x35, 0xFF, 0xF4, 0xBC, 0x36, 0xFF, 0xEF, + 0xBA, 0x37, 0xFF, 0xF5, 0xB7, 0x35, 0xFF, 0xFC, 0xB5, 0x34, 0xFF, 0xF8, 0xB5, 0x2B, 0xFF, 0xF5, 0xB6, 0x22, 0xFF, + 0xFA, 0xB5, 0x25, 0xFF, 0xFF, 0xB3, 0x28, 0xFF, 0xFF, 0xB5, 0x28, 0xFF, 0xFF, 0xB7, 0x28, 0xFF, 0xFF, 0xB4, 0x1E, + 0xFF, 0xFE, 0xB2, 0x14, 0xFF, 0xF6, 0xAD, 0x20, 0xFF, 0xFE, 0xB9, 0x3C, 0xFF, 0xF0, 0xCB, 0x5A, 0xFF, 0xFA, 0xBE, + 0x41, 0xFF, 0xFC, 0xB5, 0x29, 0xFF, 0xFE, 0xAD, 0x11, 0xFF, 0xFC, 0xAC, 0x17, 0xFF, 0xFA, 0xAB, 0x1D, 0xFF, 0xFD, + 0xA9, 0x1D, 0xFF, 0xFF, 0xA7, 0x1D, 0xFF, 0xFA, 0xA7, 0x1B, 0xFF, 0xF4, 0xA8, 0x18, 0xFF, 0xF8, 0xA6, 0x18, 0xFF, + 0xFC, 0xA4, 0x17, 0xFF, 0xFA, 0xA2, 0x19, 0xFF, 0xF7, 0xA1, 0x1A, 0xFF, 0xF7, 0xA0, 0x19, 0xFF, 0xF7, 0xA0, 0x17, + 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, + 0x13, 0xFF, 0xF5, 0x9B, 0x12, 0xFF, 0xF4, 0x9A, 0x11, 0xFF, 0xF3, 0x99, 0x10, 0xFF, 0xF3, 0x99, 0x10, 0xFF, 0xF7, + 0xD1, 0x54, 0xFF, 0xF8, 0xD0, 0x52, 0xFF, 0xF8, 0xD0, 0x51, 0xFF, 0xF9, 0xCF, 0x4F, 0xFF, 0xF9, 0xCF, 0x4E, 0xFF, + 0xF9, 0xCE, 0x4B, 0xFF, 0xF9, 0xCD, 0x49, 0xFF, 0xF9, 0xCC, 0x47, 0xFF, 0xF8, 0xCA, 0x45, 0xFF, 0xF8, 0xCA, 0x44, + 0xFF, 0xF8, 0xC9, 0x44, 0xFF, 0xF9, 0xC8, 0x43, 0xFF, 0xF9, 0xC7, 0x42, 0xFF, 0xF9, 0xC7, 0x42, 0xFF, 0xF9, 0xC6, + 0x41, 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, + 0xC1, 0x3B, 0xFF, 0xF9, 0xC0, 0x39, 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF8, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x35, 0xFF, + 0xF8, 0xBD, 0x34, 0xFF, 0xF6, 0xBC, 0x34, 0xFF, 0xF4, 0xBA, 0x35, 0xFF, 0xF8, 0xB8, 0x34, 0xFF, 0xFB, 0xB6, 0x33, + 0xFF, 0xF9, 0xB6, 0x2D, 0xFF, 0xF6, 0xB6, 0x28, 0xFF, 0xF8, 0xB5, 0x29, 0xFF, 0xFA, 0xB4, 0x29, 0xFF, 0xFB, 0xB4, + 0x29, 0xFF, 0xFC, 0xB5, 0x29, 0xFF, 0xF5, 0xB2, 0x29, 0xFF, 0xEF, 0xAF, 0x29, 0xFF, 0xF5, 0xA9, 0x1A, 0xFF, 0xD9, + 0xCE, 0x9A, 0xFF, 0xE8, 0xCF, 0x6C, 0xFF, 0xE3, 0xC6, 0x73, 0xFF, 0xDD, 0xC9, 0x7F, 0xFF, 0xFB, 0xAD, 0x18, 0xFF, + 0xF9, 0xAC, 0x1B, 0xFF, 0xF7, 0xAB, 0x1F, 0xFF, 0xF9, 0xA9, 0x1E, 0xFF, 0xFB, 0xA7, 0x1D, 0xFF, 0xF8, 0xA7, 0x1C, + 0xFF, 0xF6, 0xA6, 0x1A, 0xFF, 0xF8, 0xA5, 0x19, 0xFF, 0xFA, 0xA3, 0x19, 0xFF, 0xF9, 0xA2, 0x1A, 0xFF, 0xF8, 0xA1, + 0x1A, 0xFF, 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, 0xF7, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, + 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, 0xFF, 0xF5, 0x9B, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, + 0xF4, 0x9A, 0x10, 0xFF, 0xF4, 0x9A, 0x10, 0xFF, 0xF9, 0xD0, 0x54, 0xFF, 0xF9, 0xCF, 0x52, 0xFF, 0xF9, 0xCF, 0x51, + 0xFF, 0xFA, 0xCF, 0x4F, 0xFF, 0xFA, 0xCF, 0x4D, 0xFF, 0xF9, 0xCE, 0x4A, 0xFF, 0xF9, 0xCC, 0x48, 0xFF, 0xF8, 0xCB, + 0x46, 0xFF, 0xF8, 0xCA, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC8, 0x42, 0xFF, 0xF8, + 0xC7, 0x42, 0xFF, 0xF8, 0xC7, 0x41, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, + 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, 0xC1, 0x3A, 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF9, 0xBF, 0x37, + 0xFF, 0xF9, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x34, 0xFF, 0xF8, 0xBD, 0x33, 0xFF, 0xF9, 0xBC, 0x33, 0xFF, 0xF9, 0xBA, + 0x32, 0xFF, 0xFA, 0xB9, 0x32, 0xFF, 0xFB, 0xB7, 0x31, 0xFF, 0xF9, 0xB6, 0x30, 0xFF, 0xF8, 0xB6, 0x2E, 0xFF, 0xF6, + 0xB5, 0x2C, 0xFF, 0xF4, 0xB4, 0x2A, 0xFF, 0xF5, 0xB3, 0x2A, 0xFF, 0xF6, 0xB2, 0x2A, 0xFF, 0xF9, 0xB2, 0x29, 0xFF, + 0xFB, 0xB2, 0x28, 0xFF, 0xF6, 0xB2, 0x30, 0xFF, 0xFD, 0xA8, 0x11, 0xFF, 0xE1, 0xD3, 0x7E, 0xFF, 0xE6, 0xBB, 0x58, + 0xFF, 0xFB, 0xAA, 0x15, 0xFF, 0xF7, 0xAD, 0x1F, 0xFF, 0xF6, 0xAB, 0x1F, 0xFF, 0xF5, 0xAA, 0x20, 0xFF, 0xF6, 0xA9, + 0x1F, 0xFF, 0xF6, 0xA7, 0x1E, 0xFF, 0xF7, 0xA6, 0x1D, 0xFF, 0xF8, 0xA5, 0x1B, 0xFF, 0xF8, 0xA4, 0x1B, 0xFF, 0xF8, + 0xA3, 0x1B, 0xFF, 0xF8, 0xA2, 0x1B, 0xFF, 0xF9, 0xA1, 0x1A, 0xFF, 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, + 0xF8, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, + 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF9, 0xD0, + 0x54, 0xFF, 0xF9, 0xCF, 0x52, 0xFF, 0xF9, 0xCF, 0x51, 0xFF, 0xFA, 0xCF, 0x4F, 0xFF, 0xFA, 0xCF, 0x4D, 0xFF, 0xF9, + 0xCE, 0x4A, 0xFF, 0xF9, 0xCC, 0x48, 0xFF, 0xF8, 0xCB, 0x46, 0xFF, 0xF8, 0xCA, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, + 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC8, 0x42, 0xFF, 0xF8, 0xC7, 0x42, 0xFF, 0xF8, 0xC7, 0x41, 0xFF, 0xF9, 0xC6, 0x41, + 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, 0xC1, + 0x3A, 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF9, 0xBF, 0x37, 0xFF, 0xF9, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x34, 0xFF, 0xF8, + 0xBD, 0x33, 0xFF, 0xF9, 0xBC, 0x33, 0xFF, 0xF9, 0xBA, 0x32, 0xFF, 0xFA, 0xB9, 0x32, 0xFF, 0xFB, 0xB7, 0x31, 0xFF, + 0xF9, 0xB6, 0x30, 0xFF, 0xF8, 0xB6, 0x2E, 0xFF, 0xF6, 0xB5, 0x2C, 0xFF, 0xF4, 0xB4, 0x2A, 0xFF, 0xF5, 0xB3, 0x2A, + 0xFF, 0xF6, 0xB2, 0x2A, 0xFF, 0xF8, 0xB2, 0x2A, 0xFF, 0xFA, 0xB1, 0x29, 0xFF, 0xF4, 0xB5, 0x2D, 0xFF, 0xF5, 0xB4, + 0x1D, 0xFF, 0xFF, 0x9B, 0x23, 0xFF, 0xF2, 0xB5, 0x1F, 0xFF, 0xFB, 0xAB, 0x0B, 0xFF, 0xF6, 0xAC, 0x1E, 0xFF, 0xF6, + 0xAB, 0x1F, 0xFF, 0xF5, 0xAA, 0x20, 0xFF, 0xF6, 0xA9, 0x1F, 0xFF, 0xF6, 0xA7, 0x1E, 0xFF, 0xF7, 0xA6, 0x1D, 0xFF, + 0xF8, 0xA5, 0x1B, 0xFF, 0xF8, 0xA4, 0x1B, 0xFF, 0xF8, 0xA3, 0x1B, 0xFF, 0xF8, 0xA2, 0x1B, 0xFF, 0xF9, 0xA1, 0x1A, + 0xFF, 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, 0xF8, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, + 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, + 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF9, 0xD0, 0x54, 0xFF, 0xF9, 0xCF, 0x52, 0xFF, 0xF9, 0xCF, 0x51, 0xFF, + 0xFA, 0xCF, 0x4F, 0xFF, 0xFA, 0xCF, 0x4D, 0xFF, 0xF9, 0xCE, 0x4A, 0xFF, 0xF9, 0xCC, 0x48, 0xFF, 0xF8, 0xCB, 0x46, + 0xFF, 0xF8, 0xCA, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, 0xC8, 0x42, 0xFF, 0xF8, 0xC7, + 0x42, 0xFF, 0xF8, 0xC7, 0x41, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, + 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, 0xC1, 0x3A, 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF9, 0xBF, 0x37, 0xFF, + 0xF9, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x34, 0xFF, 0xF8, 0xBD, 0x33, 0xFF, 0xF9, 0xBC, 0x33, 0xFF, 0xF9, 0xBA, 0x32, + 0xFF, 0xFA, 0xB9, 0x32, 0xFF, 0xFB, 0xB7, 0x31, 0xFF, 0xF9, 0xB6, 0x30, 0xFF, 0xF8, 0xB6, 0x2E, 0xFF, 0xF6, 0xB5, + 0x2C, 0xFF, 0xF4, 0xB4, 0x2A, 0xFF, 0xF5, 0xB3, 0x2A, 0xFF, 0xF6, 0xB2, 0x2A, 0xFF, 0xF7, 0xB2, 0x2A, 0xFF, 0xF8, + 0xB1, 0x2A, 0xFF, 0xF9, 0xAE, 0x21, 0xFF, 0xFA, 0xAC, 0x18, 0xFF, 0xF6, 0xAD, 0x1E, 0xFF, 0xF3, 0xAE, 0x23, 0xFF, + 0xF4, 0xAC, 0x20, 0xFF, 0xF5, 0xAB, 0x1D, 0xFF, 0xF5, 0xAA, 0x1E, 0xFF, 0xF5, 0xAA, 0x20, 0xFF, 0xF6, 0xA9, 0x1F, + 0xFF, 0xF6, 0xA7, 0x1E, 0xFF, 0xF7, 0xA6, 0x1D, 0xFF, 0xF8, 0xA5, 0x1B, 0xFF, 0xF8, 0xA4, 0x1B, 0xFF, 0xF8, 0xA3, + 0x1B, 0xFF, 0xF8, 0xA2, 0x1B, 0xFF, 0xF9, 0xA1, 0x1A, 0xFF, 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, 0xF8, + 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, 0xFF, + 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF9, 0xD0, 0x54, + 0xFF, 0xF9, 0xCF, 0x52, 0xFF, 0xF9, 0xCF, 0x51, 0xFF, 0xFA, 0xCF, 0x4F, 0xFF, 0xFA, 0xCF, 0x4D, 0xFF, 0xF9, 0xCE, + 0x4A, 0xFF, 0xF9, 0xCC, 0x48, 0xFF, 0xF8, 0xCB, 0x46, 0xFF, 0xF8, 0xCA, 0x43, 0xFF, 0xF8, 0xC9, 0x43, 0xFF, 0xF8, + 0xC9, 0x43, 0xFF, 0xF8, 0xC8, 0x42, 0xFF, 0xF8, 0xC7, 0x42, 0xFF, 0xF8, 0xC7, 0x41, 0xFF, 0xF9, 0xC6, 0x41, 0xFF, + 0xF9, 0xC5, 0x40, 0xFF, 0xF9, 0xC4, 0x40, 0xFF, 0xF9, 0xC3, 0x3E, 0xFF, 0xF9, 0xC2, 0x3C, 0xFF, 0xF9, 0xC1, 0x3A, + 0xFF, 0xF9, 0xBF, 0x38, 0xFF, 0xF9, 0xBF, 0x37, 0xFF, 0xF9, 0xBE, 0x36, 0xFF, 0xF8, 0xBE, 0x34, 0xFF, 0xF8, 0xBD, + 0x33, 0xFF, 0xF9, 0xBC, 0x33, 0xFF, 0xF9, 0xBA, 0x32, 0xFF, 0xFA, 0xB9, 0x32, 0xFF, 0xFB, 0xB7, 0x31, 0xFF, 0xF9, + 0xB6, 0x30, 0xFF, 0xF8, 0xB6, 0x2E, 0xFF, 0xF6, 0xB5, 0x2C, 0xFF, 0xF4, 0xB4, 0x2A, 0xFF, 0xF5, 0xB3, 0x2A, 0xFF, + 0xF6, 0xB2, 0x2A, 0xFF, 0xF7, 0xB2, 0x2A, 0xFF, 0xF8, 0xB1, 0x2A, 0xFF, 0xF9, 0xAE, 0x21, 0xFF, 0xFA, 0xAC, 0x18, + 0xFF, 0xF6, 0xAD, 0x1E, 0xFF, 0xF3, 0xAE, 0x23, 0xFF, 0xF4, 0xAC, 0x20, 0xFF, 0xF5, 0xAB, 0x1D, 0xFF, 0xF5, 0xAA, + 0x1E, 0xFF, 0xF5, 0xAA, 0x20, 0xFF, 0xF6, 0xA9, 0x1F, 0xFF, 0xF6, 0xA7, 0x1E, 0xFF, 0xF7, 0xA6, 0x1D, 0xFF, 0xF8, + 0xA5, 0x1B, 0xFF, 0xF8, 0xA4, 0x1B, 0xFF, 0xF8, 0xA3, 0x1B, 0xFF, 0xF8, 0xA2, 0x1B, 0xFF, 0xF9, 0xA1, 0x1A, 0xFF, + 0xF8, 0xA1, 0x19, 0xFF, 0xF8, 0xA0, 0x18, 0xFF, 0xF8, 0x9F, 0x16, 0xFF, 0xF7, 0x9F, 0x15, 0xFF, 0xF7, 0x9E, 0x14, + 0xFF, 0xF6, 0x9D, 0x13, 0xFF, 0xF6, 0x9C, 0x12, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, 0xF5, 0x9A, + 0x11, 0xFF, 0xF5, 0x9A, 0x11, 0xFF, +]; diff --git a/crates/ironrdp-testsuite-extra/Cargo.toml b/crates/ironrdp-testsuite-extra/Cargo.toml new file mode 100644 index 00000000..7022d4c3 --- /dev/null +++ b/crates/ironrdp-testsuite-extra/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ironrdp-testsuite-extra" +version = "0.1.0" +edition.workspace = true +description = "IronRDP extra test suite" +publish = false +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[dev-dependencies] +anyhow = "1.0" +async-trait = "0.1" +ironrdp = { path = "../ironrdp", features = ["server", "pdu", "connector", "session", "connector"] } +ironrdp-async.path = "../ironrdp-async" +ironrdp-tokio.path = "../ironrdp-tokio" +ironrdp-tls = { path = "../ironrdp-tls", features = ["rustls"] } +semver = "1.0" +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tokio = { version = "1", features = ["sync", "time"] } + +[lints] +workspace = true diff --git a/crates/ironrdp-testsuite-extra/src/lib.rs b/crates/ironrdp-testsuite-extra/src/lib.rs new file mode 100644 index 00000000..d04a355d --- /dev/null +++ b/crates/ironrdp-testsuite-extra/src/lib.rs @@ -0,0 +1 @@ +#![allow(unused_crate_dependencies)] diff --git a/crates/ironrdp-testsuite-extra/tests/certs/Makefile b/crates/ironrdp-testsuite-extra/tests/certs/Makefile new file mode 100644 index 00000000..94bdc6e6 --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/certs/Makefile @@ -0,0 +1,17 @@ +CERT_KEY=server-key.pem +CERT_FILE=server-cert.pem +DAYS=365 +RSA_BITS=2048 +SUBJECT=/C=US/ST=Test/L=Test/O=Test/OU=Test/CN=localhost + +.PHONY: all clean certs + +all: $(CERT_KEY) $(CERT_FILE) + +$(CERT_KEY) $(CERT_FILE): + openssl req -x509 -nodes -days $(DAYS) -newkey rsa:$(RSA_BITS) \ + -keyout $(CERT_KEY) -out $(CERT_FILE) \ + -subj "$(SUBJECT)" + +clean: + rm -f $(CERT_KEY) $(CERT_FILE) diff --git a/crates/ironrdp-testsuite-extra/tests/certs/README.md b/crates/ironrdp-testsuite-extra/tests/certs/README.md new file mode 100644 index 00000000..7d0de119 --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/certs/README.md @@ -0,0 +1,3 @@ +The server-cert.pem and server-key.pem provided in this repository are +self-signed and for testing purposes only. They should not be used in production +environments. diff --git a/crates/ironrdp-testsuite-extra/tests/certs/server-cert.pem b/crates/ironrdp-testsuite-extra/tests/certs/server-cert.pem new file mode 100644 index 00000000..c49f234e --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/certs/server-cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDmzCCAoOgAwIBAgIULZzx65W6IGBs2QEidYv0gEmDNP0wDQYJKoZIhvcNAQEL +BQAwXTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx +DTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxEjAQBgNVBAMMCWxvY2FsaG9z +dDAeFw0yNTAxMjEwODA1MjFaFw0yNjAxMjEwODA1MjFaMF0xCzAJBgNVBAYTAlVT +MQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MQ0wCwYDVQQKDARUZXN0MQ0w +CwYDVQQLDARUZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDGzEhx7mcSqPmCjcYFNqmCi6JZijXsmm28fU3yQaQm +ez3/sZ3SxKucCARDBlS74YHjbcDsFq+w58cOs4XT+beKVsDSxhF1Ac+RpkXGtg2V +n/BjnN73aOUNs+GSmupA5GL6kRThKlzkV56M/5Nl0MUVwsurJ9kxLxUa7724NyYX +InJRzQENQDt9Z/QkiJC+C2G7O8W/LNTCUtqvH/BEKMzvBHxkaNGyUtfpHXu2BL43 +y2G366nIAiJ1JLhBCV7cnvoMrCpzwZbfh8pc2fRKurXY2BWuqBwZHkHM+ajE+3hj +y/0Flsa8GQ1xC7zo7MVkw9sXE40wJ8Gc9Ur61jpl8925AgMBAAGjUzBRMB0GA1Ud +DgQWBBQ9sSyrLM0nc/jBpH4A/mN2sdU2mTAfBgNVHSMEGDAWgBQ9sSyrLM0nc/jB +pH4A/mN2sdU2mTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAn +VjBEnjNR/GMcCF+JYw6EBc3sYjDZjjIjdvvbSXnF8rIdR+hxC+HI7A/6I3p+CWU1 +T3jZ9f76hqj4BjFAdqI1swypRM16qU21+4NPrA3raGZj9PRmpk1pH4fva0NaHCOA +l9tl2wSHF/wzV12Juh8hv5HTHjik2p6Ym9qKS9nkCp41CvRHVgylpQjGRDNR78n1 +yCBrQhdjIZxdFLVbIx3tIvQU1AS4igbTwTOTPuniZ1/QDRRiWS8hXM8KyhduXLB5 +0LdiupVChR9M4D7XxlMU3DQogGSxt/X9Kf4BckwztVXSGzUcTVOUizqpKIh3Gkho +DkD+onTOFXzLszn1UuzH +-----END CERTIFICATE----- diff --git a/crates/ironrdp-testsuite-extra/tests/certs/server-key.pem b/crates/ironrdp-testsuite-extra/tests/certs/server-key.pem new file mode 100644 index 00000000..05005f9f --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/certs/server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDGzEhx7mcSqPmC +jcYFNqmCi6JZijXsmm28fU3yQaQmez3/sZ3SxKucCARDBlS74YHjbcDsFq+w58cO +s4XT+beKVsDSxhF1Ac+RpkXGtg2Vn/BjnN73aOUNs+GSmupA5GL6kRThKlzkV56M +/5Nl0MUVwsurJ9kxLxUa7724NyYXInJRzQENQDt9Z/QkiJC+C2G7O8W/LNTCUtqv +H/BEKMzvBHxkaNGyUtfpHXu2BL43y2G366nIAiJ1JLhBCV7cnvoMrCpzwZbfh8pc +2fRKurXY2BWuqBwZHkHM+ajE+3hjy/0Flsa8GQ1xC7zo7MVkw9sXE40wJ8Gc9Ur6 +1jpl8925AgMBAAECggEAT7ZhBCISfWZ46dL8SGHjNV/VIO8s8Sr4/oAGDbIpZm67 +bPgk7vsCTsXeI5v5xP5G7VE4btIn75j4ddohOt6iLFvd5IYcQN0RhHb1+phMOSdR +JjgkJXOPiN+MfxMUBCIv2AXtp92rMro5bpMaYNSF+lRKA16ulayp20uvOJsQcGyg +EDhhaLB+pEwEDGrHLxP1O+DY1Ukc13sWObNwVEjW7qtTf+cach9DqliWU9nkyO6X +SZ8mSX5ki0zVJPCtBw4z95m869Alzr/vKk91zvkdKJM1PZTppon2nRvq2AIvgPOa +NAbY9zSgBoVHILmzl6vlpkHHLV+D1JyPlOdT9xh8swKBgQDm7SUpqOw0ZDQFK6ql +5P3I2YglbUR+/MCNPUANRtoYOkEzwBXyhtx1/0C1Ieh63H9W/5wlnswSYWv+ZMnY +ZULzxZH+jtmyspJFKnSaoCDH9lAa1+50kgJI5/B1jNpRq2jZLP4Kl5bPhb9Mu29D +bPbRqXLJtr+JL41bq+CKKdJ/bwKBgQDcYhgVzpVBcHQsJX2q2HoabgRXBDZbHT5Z +uzbRP1rwgY6w5D5aJKub4E2gwHF1b6JfIXCda4qXkaywyH+WcgdJBBEfAViqh4z1 +HPoxsknPtTLwyEvNthQJOyD684mD3D2uaEmpKQIyvm4ESYTjjYhVocqKvIu2c3Or ++nIRdDbhVwKBgQCPM+aE1CVORAliX3bek4exwvxDwWPln9XEgIQ094gN2CpQ7kBt ++qXCYrz81n81mYE6MR7i0XvZtiJjSptFH16Kjy1+/5UO1OASFkbjEIPjnOKGEvvj +vBvAnFyoeOV2GebWLqmHZgP2wwkji2RvGqZg1ETDxBk4+I0fmRGQfGj17wKBgQDV +P1oc58vHCYBwI0rpYRUts90hMiNCoRZvD1eovAxMAqFHC2RGJ4uihjW3Yd+nigDs +2le1C5WMuloGqcvDkMz52ySSAuSABi/gEk0Kf4EqqiQDl1y6TgAvOnbcPYGIBTnu +JF16gQLuhRPBtD4RTido7OgmvPDX9/kqpWlw+CoOewKBgQCbVMk+exyz1e4BrxB8 +vcZzVfOfwiDn8GnCoD32aL/JbO5ize6PIVm+zTsIuzv3hLDHoIs2/CxX7O3gI8eL +Hxn6JF9mwOE4uWcCTVYXZIwpqvrsTu0EUhRrCqLdA05pbBxIfODhb45RlB5vSn7G +Lt3YRdYjbU/R2YL3e8HMaUSwyA== +-----END PRIVATE KEY----- diff --git a/crates/ironrdp-testsuite-extra/tests/mod.rs b/crates/ironrdp-testsuite-extra/tests/mod.rs new file mode 100644 index 00000000..8cc24fab --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/mod.rs @@ -0,0 +1,310 @@ +#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary +#![allow(clippy::unwrap_used, reason = "unwrap is fine in tests")] + +use core::future::Future; +use std::path::Path; +use std::sync::Arc; + +use anyhow::Result; +use ironrdp::connector; +use ironrdp::pdu::rdp::capability_sets::MajorPlatformType; +use ironrdp::pdu::{self, gcc}; +use ironrdp::server::{ + self, DesktopSize, DisplayUpdate, KeyboardEvent, MouseEvent, PixelFormat, RdpServer, RdpServerDisplay, + RdpServerDisplayUpdates, RdpServerInputHandler, ServerEvent, TlsIdentityCtx, +}; +use ironrdp::session::image::DecodedImage; +use ironrdp::session::{self, ActiveStage, ActiveStageOutput}; +use ironrdp_async::{Framed, FramedWrite as _}; +use ironrdp_testsuite_extra as _; +use ironrdp_tls::TlsStream; +use ironrdp_tokio::TokioStream; +use tokio::net::TcpStream; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use tokio::sync::{oneshot, Mutex}; +use tracing::debug; + +const DESKTOP_WIDTH: u16 = 1024; +const DESKTOP_HEIGHT: u16 = 768; +const USERNAME: &str = ""; +const PASSWORD: &str = ""; + +#[tokio::test] +async fn test_client_server() { + client_server(default_client_config(), |stage, framed, _display_tx| async { + (stage, framed) + }) + .await +} + +#[tokio::test] +async fn test_deactivation_reactivation() { + let client_config = default_client_config(); + let mut image = DecodedImage::new( + PixelFormat::RgbA32, + client_config.desktop_size.width, + client_config.desktop_size.height, + ); + client_server(client_config, |mut stage, mut framed, display_tx| async move { + display_tx + .send(DisplayUpdate::Resize(DesktopSize { + width: 2048, + height: 2048, + })) + .unwrap(); + { + let (action, payload) = framed.read_pdu().await.expect("valid PDU"); + let outputs = stage.process(&mut image, action, &payload).expect("stage process"); + let out = outputs.into_iter().next().unwrap(); + match out { + ActiveStageOutput::DeactivateAll(mut connection_activation) => { + // TODO: factor this out in common client code + // Execute the Deactivation-Reactivation Sequence: + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432 + debug!("Received Server Deactivate All PDU, executing Deactivation-Reactivation Sequence"); + let mut buf = pdu::WriteBuf::new(); + 'activation_seq: loop { + let written = ironrdp_async::single_sequence_step_read( + &mut framed, + &mut *connection_activation, + &mut buf, + ) + .await + .map_err(|e| session::custom_err!("read deactivation-reactivation sequence step", e)) + .unwrap(); + + if written.size().is_some() { + framed + .write_all(buf.filled()) + .await + .map_err(|e| session::custom_err!("write deactivation-reactivation sequence step", e)) + .unwrap(); + } + + if let connector::connection_activation::ConnectionActivationState::Finalized { + io_channel_id, + user_channel_id, + desktop_size, + enable_server_pointer, + pointer_software_rendering, + } = connection_activation.connection_activation_state() + { + debug!(?desktop_size, "Deactivation-Reactivation Sequence completed"); + // Update image size with the new desktop size. + // image = DecodedImage::new(PixelFormat::RgbA32, desktop_size.width, desktop_size.height); + // Update the active stage with the new channel IDs and pointer settings. + stage.set_fastpath_processor( + session::fast_path::ProcessorBuilder { + io_channel_id, + user_channel_id, + enable_server_pointer, + pointer_software_rendering, + } + .build(), + ); + stage.set_enable_server_pointer(enable_server_pointer); + break 'activation_seq; + } + } + } + _ => unreachable!(), + } + } + (stage, framed) + }) + .await +} + +type DisplayUpdatesRx = Arc>>; + +struct TestDisplayUpdates { + rx: DisplayUpdatesRx, +} + +#[async_trait::async_trait] +impl RdpServerDisplayUpdates for TestDisplayUpdates { + async fn next_update(&mut self) -> Result> { + let mut rx = self.rx.lock().await; + + Ok(rx.recv().await) + } +} + +struct TestDisplay { + rx: DisplayUpdatesRx, +} + +#[async_trait::async_trait] +impl RdpServerDisplay for TestDisplay { + async fn size(&mut self) -> DesktopSize { + DesktopSize { + width: DESKTOP_WIDTH, + height: DESKTOP_HEIGHT, + } + } + + async fn updates(&mut self) -> Result> { + Ok(Box::new(TestDisplayUpdates { + rx: Arc::clone(&self.rx), + })) + } +} + +struct TestInputHandler; +impl RdpServerInputHandler for TestInputHandler { + fn keyboard(&mut self, _: KeyboardEvent) {} + fn mouse(&mut self, _: MouseEvent) {} +} + +async fn client_server(client_config: connector::Config, clientfn: F) +where + F: FnOnce(ActiveStage, Framed>>, UnboundedSender) -> Fut + 'static, + Fut: Future>>)>, +{ + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); + + let cert_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/certs/server-cert.pem"); + let key_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/certs/server-key.pem"); + let identity = TlsIdentityCtx::init_from_paths(&cert_path, &key_path).expect("failed to init TLS identity"); + let acceptor = identity.make_acceptor().expect("failed to build TLS acceptor"); + + let (display_tx, display_rx) = mpsc::unbounded_channel(); + let mut server = RdpServer::builder() + .with_addr(([127, 0, 0, 1], 0)) + .with_tls(acceptor) + .with_input_handler(TestInputHandler) + .with_display_handler(TestDisplay { + rx: Arc::new(Mutex::new(display_rx)), + }) + .build(); + server.set_credentials(Some(server::Credentials { + username: USERNAME.into(), + password: PASSWORD.into(), + domain: None, + })); + let ev = server.event_sender().clone(); + + let local = tokio::task::LocalSet::new(); + local + .run_until(async move { + let server = tokio::task::spawn_local(async move { + server.run().await.unwrap(); + }); + + let client = tokio::task::spawn_local(async move { + let (tx, rx) = oneshot::channel(); + ev.send(ServerEvent::GetLocalAddr(tx)).unwrap(); + let server_addr = rx.await.unwrap().unwrap(); + let tcp_stream = TcpStream::connect(server_addr).await.expect("TCP connect"); + let client_addr = tcp_stream.local_addr().expect("local_addr"); + let mut framed = ironrdp_tokio::TokioFramed::new(tcp_stream); + let mut connector = connector::ClientConnector::new(client_config, client_addr); + let should_upgrade = ironrdp_async::connect_begin(&mut framed, &mut connector) + .await + .expect("begin connection"); + let initial_stream = framed.into_inner_no_leftover(); + let (upgraded_stream, tls_cert) = ironrdp_tls::upgrade(initial_stream, "localhost") + .await + .expect("TLS upgrade"); + let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, &mut connector); + let mut upgraded_framed = ironrdp_tokio::TokioFramed::new(upgraded_stream); + let server_public_key = + ironrdp_tls::extract_tls_server_public_key(&tls_cert).expect("extract server public key"); + let connection_result = ironrdp_async::connect_finalize( + upgraded, + connector, + &mut upgraded_framed, + &mut ironrdp_tokio::reqwest::ReqwestNetworkClient::new(), + "localhost".into(), + server_public_key.to_owned(), + None, + ) + .await + .expect("finalize connection"); + + let active_stage = ActiveStage::new(connection_result); + let (active_stage, mut upgraded_framed) = clientfn(active_stage, upgraded_framed, display_tx).await; + let outputs = active_stage.graceful_shutdown().expect("shutdown"); + for out in outputs { + match out { + ActiveStageOutput::ResponseFrame(frame) => { + upgraded_framed.write_all(&frame).await.expect("write frame"); + } + _ => unimplemented!(), + } + } + + // server should probably send TLS close_notify + while let Ok(pdu) = upgraded_framed.read_pdu().await { + debug!(?pdu); + } + ev.send(ServerEvent::Quit("bye".into())).unwrap(); + }); + + tokio::try_join!(server, client).expect("join"); + }) + .await; +} + +// Maybe implement Default for Config +fn default_client_config() -> connector::Config { + connector::Config { + desktop_size: DesktopSize { + width: DESKTOP_WIDTH, + height: DESKTOP_HEIGHT, + }, + desktop_scale_factor: 0, // Default to 0 per FreeRDP + enable_tls: true, + enable_credssp: true, + credentials: connector::Credentials::UsernamePassword { + username: USERNAME.into(), + password: PASSWORD.into(), + }, + domain: None, + client_build: semver::Version::parse(env!("CARGO_PKG_VERSION")) + .map(|version| version.major * 100 + version.minor * 10 + version.patch) + .unwrap_or(0) + .try_into() + .unwrap(), + client_name: "ironrdp".into(), + keyboard_type: gcc::KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_layout: 0, + keyboard_functional_keys_count: 12, + ime_file_name: "".into(), + bitmap: None, + dig_product_id: "".into(), + // NOTE: hardcode this value like in freerdp + // https://github.com/FreeRDP/FreeRDP/blob/4e24b966c86fdf494a782f0dfcfc43a057a2ea60/libfreerdp/core/settings.c#LL49C34-L49C70 + client_dir: "C:\\Windows\\System32\\mstscax.dll".into(), + #[cfg(windows)] + platform: MajorPlatformType::WINDOWS, + #[cfg(target_os = "macos")] + platform: MajorPlatformType::MACINTOSH, + #[cfg(target_os = "ios")] + platform: MajorPlatformType::IOS, + #[cfg(target_os = "linux")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "android")] + platform: MajorPlatformType::ANDROID, + #[cfg(target_os = "freebsd")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "dragonfly")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "openbsd")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "netbsd")] + platform: MajorPlatformType::UNIX, + hardware_id: None, + request_data: None, + autologon: false, + enable_audio_playback: true, + license_cache: None, + enable_server_pointer: true, + pointer_software_rendering: true, + performance_flags: Default::default(), + timezone_info: Default::default(), + } +} diff --git a/crates/ironrdp-tls/CHANGELOG.md b/crates/ironrdp-tls/CHANGELOG.md new file mode 100644 index 00000000..2aaa1c25 --- /dev/null +++ b/crates/ironrdp-tls/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-tls-v0.1.4...ironrdp-tls-v0.2.0)] - 2025-12-18 + +### Features + +- [**breaking**] Return x509_cert::Certificate from upgrade() ([#1054](https://github.com/Devolutions/IronRDP/issues/1054)) ([bd2aed7686](https://github.com/Devolutions/IronRDP/commit/bd2aed76867f4038c32df9a0d24532ee40d2f14c)) + + This allows client applications to verify details of the certificate, + possibly with the user, when connecting to a server using TLS. + +## [[0.1.4](https://github.com/Devolutions/IronRDP/compare/ironrdp-tls-v0.1.3...ironrdp-tls-v0.1.4)] - 2025-08-29 + +### Build + +- Bump tokio from 1.46.1 to 1.47.0 (#893) ([5d513dcf09](https://github.com/Devolutions/IronRDP/commit/5d513dcf099505d4d52fe25884dc019590bc751e)) + +## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-tls-v0.1.1...ironrdp-tls-v0.1.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +### Build + +- Bump tokio from 1.42.0 to 1.43.0 (#650) ([ff6c6e875b](https://github.com/Devolutions/IronRDP/commit/ff6c6e875b4c2dce7ec109c3721739f86a808a31)) + +## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-tls-v0.1.0...ironrdp-tls-v0.1.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-tls/Cargo.toml b/crates/ironrdp-tls/Cargo.toml new file mode 100644 index 00000000..1c719be0 --- /dev/null +++ b/crates/ironrdp-tls/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "ironrdp-tls" +version = "0.2.0" +readme = "README.md" +description = "TLS boilerplate common with most IronRDP clients" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] # No default feature, the user must choose a TLS backend by enabling the appropriate feature. +rustls = ["dep:tokio-rustls", "dep:x509-cert", "tokio/io-util"] +native-tls = ["dep:tokio-native-tls", "dep:x509-cert", "tokio/io-util"] +stub = [] + +[dependencies] +tokio = { version = "1.47" } +x509-cert = { version = "0.2", default-features = false, features = ["std"], optional = true } # public +tokio-native-tls = { version = "0.3", optional = true } # public +tokio-rustls = { version = "0.26", optional = true } # public + +[lints] +workspace = true + diff --git a/crates/ironrdp-tls/LICENSE-APACHE b/crates/ironrdp-tls/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-tls/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-tls/LICENSE-MIT b/crates/ironrdp-tls/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-tls/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-tls/README.md b/crates/ironrdp-tls/README.md new file mode 100644 index 00000000..f1f0b2bc --- /dev/null +++ b/crates/ironrdp-tls/README.md @@ -0,0 +1,63 @@ +# IronRDP TLS + +TLS boilerplate common with most IronRDP clients. + +This crate exposes three features for selecting the TLS backend: + +- `rustls`: use the rustls crate. +- `native-tls`: use the native-tls crate. +- `stub`: use a stubbed backend which fail at runtime when used. + +These features are mutually exclusive and only one may be enabled at a time. +When more than one backend is enabled, a compile-time error is emitted. +For this reason, no feature is enabled by default. + +The rationale is two-fold: + +- It makes deliberate the choice of the TLS backend. +- It eliminates the risk of mistakenly enabling multiple backends at once. + +With this approach, it’s obvious which backend is enabled when looking at the dependency declaration: + +```toml +# This: +ironrdp-tls = { version = "x.y.z", features = ["rustls"] } + +# Instead of: +ironrdp-tls = "x.y.z" +``` + +There is also no default feature to disable: + +```toml +# This: +ironrdp-tls = { version = "x.y.z", features = ["native-tls"] } + +# Instead of: +ironrdp-tls = { version = "x.y.z", default-features = false, features = ["native-tls"] } +``` + +This is typically more convenient and less error-prone when re-exposing the features from another crate. + +```toml +[features] +rustls = ["ironrdp-tls/rustls"] +native-tls = ["ironrdp-tls/native-tls"] +stub-tls = ["ironrdp-tls/stub"] + +# This: +[dependencies] +ironrdp-tls = "x.y.z" + +# Instead of: +[dependencies] +ironrdp-tls = { version = "x.y.z", default-features = false } +``` + +(This is worse when the crate is exposing other default features which are typically not disabled by default.) + +The stubbed backend is provided as an easy way to make the code compiles with minimal dependencies if required. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-tls/src/lib.rs b/crates/ironrdp-tls/src/lib.rs new file mode 100644 index 00000000..58349712 --- /dev/null +++ b/crates/ironrdp-tls/src/lib.rs @@ -0,0 +1,33 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +#[cfg(feature = "rustls")] +#[path = "rustls.rs"] +mod impl_; + +#[cfg(feature = "native-tls")] +#[path = "native_tls.rs"] +mod impl_; + +#[cfg(feature = "stub")] +#[path = "stub.rs"] +mod impl_; + +#[cfg(any( + not(any(feature = "stub", feature = "native-tls", feature = "rustls")), + all(feature = "stub", feature = "native-tls"), + all(feature = "stub", feature = "rustls"), + all(feature = "rustls", feature = "native-tls"), +))] +compile_error!("a TLS backend must be selected by enabling a single feature out of: `rustls`, `native-tls`, `stub`"); + +// The whole public API of this crate. +#[cfg(any(feature = "stub", feature = "native-tls", feature = "rustls"))] +pub use impl_::{upgrade, TlsStream}; + +pub fn extract_tls_server_public_key(cert: &x509_cert::Certificate) -> Option<&[u8]> { + cert.tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() +} diff --git a/crates/ironrdp-tls/src/native_tls.rs b/crates/ironrdp-tls/src/native_tls.rs new file mode 100644 index 00000000..f3b7d0d0 --- /dev/null +++ b/crates/ironrdp-tls/src/native_tls.rs @@ -0,0 +1,41 @@ +use std::io; + +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _}; + +pub type TlsStream = tokio_native_tls::TlsStream; + +pub async fn upgrade(stream: S, server_name: &str) -> io::Result<(TlsStream, x509_cert::Certificate)> +where + S: Unpin + AsyncRead + AsyncWrite, +{ + let mut tls_stream = { + let connector = tokio_native_tls::native_tls::TlsConnector::builder() + .danger_accept_invalid_certs(true) + .use_sni(false) + .build() + .map(tokio_native_tls::TlsConnector::from) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + connector + .connect(server_name, stream) + .await + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))? + }; + + tls_stream.flush().await?; + + let tls_cert = { + use x509_cert::der::Decode as _; + + let cert = tls_stream + .get_ref() + .peer_certificate() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))? + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "peer certificate is missing"))?; + let cert = cert.to_der().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + x509_cert::Certificate::from_der(&cert).map_err(io::Error::other)? + }; + + Ok((tls_stream, tls_cert)) +} diff --git a/crates/ironrdp-tls/src/rustls.rs b/crates/ironrdp-tls/src/rustls.rs new file mode 100644 index 00000000..ca8778d5 --- /dev/null +++ b/crates/ironrdp-tls/src/rustls.rs @@ -0,0 +1,109 @@ +use std::io; + +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _}; +use tokio_rustls::rustls; +use tokio_rustls::rustls::pki_types::ServerName; + +pub type TlsStream = tokio_rustls::client::TlsStream; + +pub async fn upgrade(stream: S, server_name: &str) -> io::Result<(TlsStream, x509_cert::Certificate)> +where + S: Unpin + AsyncRead + AsyncWrite, +{ + let mut tls_stream = { + let mut config = rustls::client::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(std::sync::Arc::new(danger::NoCertificateVerification)) + .with_no_client_auth(); + + // This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret) + config.key_log = std::sync::Arc::new(rustls::KeyLogFile::new()); + + // Disable TLS resumption because it’s not supported by some services such as CredSSP. + // + // > The CredSSP Protocol does not extend the TLS wire protocol. TLS session resumption is not supported. + // + // source: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/385a7489-d46b-464c-b224-f7340e308a5c + config.resumption = rustls::client::Resumption::disabled(); + + let config = std::sync::Arc::new(config); + + let domain = ServerName::try_from(server_name.to_owned()).map_err(io::Error::other)?; + + tokio_rustls::TlsConnector::from(config).connect(domain, stream).await? + }; + + tls_stream.flush().await?; + + let tls_cert = { + use x509_cert::der::Decode as _; + + let cert = tls_stream + .get_ref() + .1 + .peer_certificates() + .and_then(|certificates| certificates.first()) + .ok_or_else(|| io::Error::other("peer certificate is missing"))?; + + x509_cert::Certificate::from_der(cert).map_err(io::Error::other)? + }; + + Ok((tls_stream, tls_cert)) +} + +mod danger { + use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; + use tokio_rustls::rustls::{pki_types, DigitallySignedStruct, Error, SignatureScheme}; + + #[derive(Debug)] + pub(super) struct NoCertificateVerification; + + impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _: &pki_types::CertificateDer<'_>, + _: &[pki_types::CertificateDer<'_>], + _: &pki_types::ServerName<'_>, + _: &[u8], + _: pki_types::UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _: &[u8], + _: &pki_types::CertificateDer<'_>, + _: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _: &[u8], + _: &pki_types::CertificateDer<'_>, + _: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } + } +} diff --git a/crates/ironrdp-tls/src/stub.rs b/crates/ironrdp-tls/src/stub.rs new file mode 100644 index 00000000..484979d6 --- /dev/null +++ b/crates/ironrdp-tls/src/stub.rs @@ -0,0 +1,39 @@ +use std::io; +use std::marker::PhantomData; +use std::task::{Context, Poll}; + +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +#[derive(Debug)] +pub struct TlsStream { + _marker: PhantomData, +} + +impl AsyncRead for TlsStream { + fn poll_read(self: std::pin::Pin<&mut Self>, _: &mut Context<'_>, _: &mut ReadBuf<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl AsyncWrite for TlsStream { + fn poll_write(self: std::pin::Pin<&mut Self>, _: &mut Context<'_>, _: &[u8]) -> Poll> { + Poll::Ready(Ok(0)) + } + + fn poll_flush(self: std::pin::Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: std::pin::Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +pub async fn upgrade(stream: S, server_name: &str) -> io::Result<(TlsStream, Vec)> +where + S: Unpin + AsyncRead + AsyncWrite, +{ + // Do nothing and fail + let _ = (stream, server_name); + Err(io::Error::other("no TLS backend enabled for this build")) +} diff --git a/crates/ironrdp-tokio/CHANGELOG.md b/crates/ironrdp-tokio/CHANGELOG.md new file mode 100644 index 00000000..db3bd881 --- /dev/null +++ b/crates/ironrdp-tokio/CHANGELOG.md @@ -0,0 +1,100 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.7.0...ironrdp-tokio-v0.8.0)] - 2025-12-18 + +### Features + +- Add MovableTokioFramed for Send+!Sync context ([#1033](https://github.com/Devolutions/IronRDP/issues/1033)) ([966ba8a53e](https://github.com/Devolutions/IronRDP/commit/966ba8a53e43a193271f40b9db80e45e495e2f24)) + + The `ironrdp-tokio` crate currently provides the following two + `Framed` implementations using the standard `tokio::io` traits: + - `type TokioFramed = Framed>` where `S: Send + Sync + + Unpin` + - `type LocalTokioFramed = Framed>` where `S: + Unpin` + + The former is meant for multi-threaded runtimes and the latter is meant + for single-threaded runtimes. + + This PR adds a third `Framed` implementation: + + `pub type MovableTokioFramed = Framed>` where + `S: Send + Unpin` + + This is a valid usecase as some implementations of the `tokio::io` + traits are `Send` but `!Sync`. Without this new third type, consumers of + `Framed` who have a `S: Send + !Sync` trait for their streams are + forced to downgrade to `LocalTokioFramed` and do some hacky workaround + with `tokio::task::spawn_blocking` since the defined associated futures, + `ReadFut` and `WriteAllFut`, are neither `Send` nor `Sync`. + +### Bug Fixes + +- [**breaking**] Use static dispatch for NetworkClient trait ([#1043](https://github.com/Devolutions/IronRDP/issues/1043)) ([bca6d190a8](https://github.com/Devolutions/IronRDP/commit/bca6d190a870708468534d224ff225a658767a9a)) + + - Rename `AsyncNetworkClient` to `NetworkClient` + - Replace dynamic dispatch (`Option<&mut dyn ...>`) with static dispatch + using generics (`&mut N where N: NetworkClient`) + - Reorder `connect_finalize` parameters for consistency across crates + +### Build + +- Bump picky and sspi ([#1028](https://github.com/Devolutions/IronRDP/issues/1028)) ([5bd319126d](https://github.com/Devolutions/IronRDP/commit/5bd319126d32fbd8e505508e27ab2b1a18a83d04)) + + This fixes build issues with some dependencies. + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.5.1...ironrdp-tokio-v0.6.0)] - 2025-07-08 + +### Build + +- Update sspi dependency (#839) ([33530212c4](https://github.com/Devolutions/IronRDP/commit/33530212c42bf28c875ac078ed2408657831b417)) + +## [[0.5.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.5.0...ironrdp-tokio-v0.5.1)] - 2025-07-08 + +### Features + +- Add async `ReqwestNetworkClient::send` method (#859) ([7e23a8bb97](https://github.com/Devolutions/IronRDP/commit/7e23a8bb97991d0e24e65d77a11d9854492ee024)) + +## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.4.0...ironrdp-tokio-v0.5.0)] - 2025-06-06 + +### Bug Fixes + +- [**breaking**] Adjust reqwest-related features (#812) ([9408789491](https://github.com/Devolutions/IronRDP/commit/9408789491b3e09b69e0aaa03fd215326b624ec0)) + + - Remove `reqwest` from the default feature set. + - Disable default TLS backend. + - Add `reqwest-rustls-ring` to enable rustls + ring backend. + - Add `reqwest-native-tls` to enable native-tls backend. + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.3.0...ironrdp-tokio-v0.4.0)] - 2025-05-27 + +### Features + +- Add reqwest feature (#734) ([032c38be92](https://github.com/Devolutions/IronRDP/commit/032c38be9229cfd35f0f6fc8eac5cccc960480d3)) + +## [[0.2.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.2.2...ironrdp-tokio-v0.2.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + + +## [[0.2.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.2.1...ironrdp-tokio-v0.2.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.2.0...ironrdp-tokio-v0.2.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/crates/ironrdp-tokio/Cargo.toml b/crates/ironrdp-tokio/Cargo.toml new file mode 100644 index 00000000..abb08162 --- /dev/null +++ b/crates/ironrdp-tokio/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "ironrdp-tokio" +version = "0.8.0" +readme = "README.md" +description = "`Framed*` traits implementation above Tokio’s traits" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +reqwest = ["dep:reqwest", "dep:sspi", "dep:url", "dep:ironrdp-connector"] +reqwest-rustls-ring = ["reqwest", "reqwest?/rustls-tls-webpki-roots"] +reqwest-native-tls = ["reqwest", "reqwest?/native-tls"] + +[dependencies] +bytes = "1" +ironrdp-async = { path = "../ironrdp-async", version = "0.8" } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.8", optional = true } +tokio = { version = "1", features = ["io-util"] } +reqwest = { version = "0.12", default-features = false, features = ["http2", "system-proxy"], optional = true } +sspi = { version = "0.18", features = ["network_client", "dns_resolver"], optional = true } +url = { version = "2.5", optional = true } + +[lints] +workspace = true diff --git a/crates/ironrdp-tokio/LICENSE-APACHE b/crates/ironrdp-tokio/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp-tokio/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp-tokio/LICENSE-MIT b/crates/ironrdp-tokio/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp-tokio/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp-tokio/README.md b/crates/ironrdp-tokio/README.md new file mode 100644 index 00000000..99a7861d --- /dev/null +++ b/crates/ironrdp-tokio/README.md @@ -0,0 +1,8 @@ +# IronRDP Tokio + +`Framed*` traits implementation above [Tokio]’s traits. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP +[Tokio]: https://tokio.rs/ diff --git a/crates/ironrdp-tokio/src/lib.rs b/crates/ironrdp-tokio/src/lib.rs new file mode 100644 index 00000000..b188e84b --- /dev/null +++ b/crates/ironrdp-tokio/src/lib.rs @@ -0,0 +1,223 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +#[rustfmt::skip] // do not re-order this pub use +pub use ironrdp_async::*; + +#[cfg(feature = "reqwest")] +pub mod reqwest; + +use core::pin::Pin; +use std::io; + +use bytes::BytesMut; +use tokio::io::{AsyncRead, AsyncWrite, ReadHalf, WriteHalf}; + +pub type TokioFramed = Framed>; + +pub fn split_tokio_framed(framed: TokioFramed) -> (TokioFramed>, TokioFramed>) +where + S: Sync + Unpin + AsyncRead + AsyncWrite, +{ + let (stream, leftover) = framed.into_inner(); + let (read_half, write_half) = tokio::io::split(stream); + let framed_read = TokioFramed::new_with_leftover(read_half, leftover); + let framed_write = TokioFramed::new(write_half); + (framed_read, framed_write) +} + +pub fn unsplit_tokio_framed(reader: TokioFramed>, writer: TokioFramed>) -> TokioFramed +where + S: Sync + Unpin + AsyncRead + AsyncWrite, +{ + let (reader, leftover) = reader.into_inner(); + let writer = writer.into_inner_no_leftover(); + TokioFramed::new_with_leftover(reader.unsplit(writer), leftover) +} + +pub struct TokioStream { + inner: S, +} + +impl StreamWrapper for TokioStream { + type InnerStream = S; + + fn from_inner(stream: Self::InnerStream) -> Self { + Self { inner: stream } + } + + fn into_inner(self) -> Self::InnerStream { + self.inner + } + + fn get_inner(&self) -> &Self::InnerStream { + &self.inner + } + + fn get_inner_mut(&mut self) -> &mut Self::InnerStream { + &mut self.inner + } +} + +impl FramedRead for TokioStream +where + S: Send + Sync + Unpin + AsyncRead, +{ + type ReadFut<'read> + = Pin> + Send + Sync + 'read>> + where + Self: 'read; + + fn read<'a>(&'a mut self, buf: &'a mut BytesMut) -> Self::ReadFut<'a> { + use tokio::io::AsyncReadExt as _; + + Box::pin(async { self.inner.read_buf(buf).await }) + } +} + +impl FramedWrite for TokioStream +where + S: Send + Sync + Unpin + AsyncWrite, +{ + type WriteAllFut<'write> + = Pin> + Send + Sync + 'write>> + where + Self: 'write; + + fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a> { + use tokio::io::AsyncWriteExt as _; + + Box::pin(async { + self.inner.write_all(buf).await?; + self.inner.flush().await?; + + Ok(()) + }) + } +} + +pub type LocalTokioFramed = Framed>; + +pub struct LocalTokioStream { + inner: S, +} + +impl StreamWrapper for LocalTokioStream { + type InnerStream = S; + + fn from_inner(stream: Self::InnerStream) -> Self { + Self { inner: stream } + } + + fn into_inner(self) -> Self::InnerStream { + self.inner + } + + fn get_inner(&self) -> &Self::InnerStream { + &self.inner + } + + fn get_inner_mut(&mut self) -> &mut Self::InnerStream { + &mut self.inner + } +} + +impl FramedRead for LocalTokioStream +where + S: Unpin + AsyncRead, +{ + type ReadFut<'read> + = Pin> + 'read>> + where + Self: 'read; + + fn read<'a>(&'a mut self, buf: &'a mut BytesMut) -> Self::ReadFut<'a> { + use tokio::io::AsyncReadExt as _; + + Box::pin(async { self.inner.read_buf(buf).await }) + } +} + +impl FramedWrite for LocalTokioStream +where + S: Unpin + AsyncWrite, +{ + type WriteAllFut<'write> + = Pin> + 'write>> + where + Self: 'write; + + fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a> { + use tokio::io::AsyncWriteExt as _; + + Box::pin(async { + self.inner.write_all(buf).await?; + self.inner.flush().await?; + + Ok(()) + }) + } +} + +pub type MovableTokioFramed = Framed>; + +pub struct MovableTokioStream { + inner: S, +} + +impl StreamWrapper for MovableTokioStream { + type InnerStream = S; + + fn from_inner(stream: Self::InnerStream) -> Self { + Self { inner: stream } + } + + fn into_inner(self) -> Self::InnerStream { + self.inner + } + + fn get_inner(&self) -> &Self::InnerStream { + &self.inner + } + + fn get_inner_mut(&mut self) -> &mut Self::InnerStream { + &mut self.inner + } +} + +impl FramedRead for MovableTokioStream +where + S: Send + Unpin + AsyncRead, +{ + type ReadFut<'read> + = Pin> + Send + 'read>> + where + Self: 'read; + + fn read<'a>(&'a mut self, buf: &'a mut BytesMut) -> Self::ReadFut<'a> { + use tokio::io::AsyncReadExt as _; + + Box::pin(async { self.inner.read_buf(buf).await }) + } +} + +impl FramedWrite for MovableTokioStream +where + S: Send + Unpin + AsyncWrite, +{ + type WriteAllFut<'write> + = Pin> + Send + 'write>> + where + Self: 'write; + + fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a> { + use tokio::io::AsyncWriteExt as _; + + Box::pin(async { + self.inner.write_all(buf).await?; + self.inner.flush().await?; + + Ok(()) + }) + } +} diff --git a/crates/ironrdp-tokio/src/reqwest.rs b/crates/ironrdp-tokio/src/reqwest.rs new file mode 100644 index 00000000..deeee192 --- /dev/null +++ b/crates/ironrdp-tokio/src/reqwest.rs @@ -0,0 +1,132 @@ +use core::net::{IpAddr, Ipv4Addr}; + +use ironrdp_connector::{custom_err, general_err, ConnectorResult}; +use reqwest::Client; +use sspi::{Error, ErrorKind}; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tokio::net::{TcpStream, UdpSocket}; +use url::Url; + +use crate::NetworkClient; + +pub struct ReqwestNetworkClient { + client: Option, +} + +impl NetworkClient for ReqwestNetworkClient { + async fn send(&mut self, network_request: &sspi::generator::NetworkRequest) -> ConnectorResult> { + ReqwestNetworkClient::send_request(self, network_request).await + } +} + +impl ReqwestNetworkClient { + pub fn new() -> Self { + Self { client: None } + } + + pub async fn send_request<'a>( + &'a mut self, + request: &'a sspi::generator::NetworkRequest, + ) -> ConnectorResult> { + match &request.protocol { + sspi::network_client::NetworkProtocol::Tcp => self.send_tcp(&request.url, &request.data).await, + sspi::network_client::NetworkProtocol::Udp => self.send_udp(&request.url, &request.data).await, + sspi::network_client::NetworkProtocol::Http | sspi::network_client::NetworkProtocol::Https => { + self.send_http(&request.url, &request.data).await + } + } + } + + async fn send_tcp(&self, url: &Url, data: &[u8]) -> ConnectorResult> { + let addr = format!("{}:{}", url.host_str().unwrap_or_default(), url.port().unwrap_or(88)); + + let mut stream = TcpStream::connect(addr) + .await + .map_err(|e| Error::new(ErrorKind::NoAuthenticatingAuthority, format!("{e:?}"))) + .map_err(|e| custom_err!("failed to send KDC request over TCP", e))?; + + stream + .write(data) + .await + .map_err(|e| Error::new(ErrorKind::NoAuthenticatingAuthority, format!("{e:?}"))) + .map_err(|e| custom_err!("failed to send KDC request over TCP", e))?; + + let len = stream + .read_u32() + .await + .map_err(|e| Error::new(ErrorKind::NoAuthenticatingAuthority, format!("{e:?}"))) + .map_err(|e| custom_err!("failed to send KDC request over TCP", e))?; + + let len = usize::try_from(len) + .map_err(|_| general_err!("invalid buffer length: out of range integral type conversion"))?; + + let mut buf = vec![0; len + 4]; + buf[0..4].copy_from_slice(&(len.to_be_bytes())); + + stream + .read_exact(&mut buf[4..]) + .await + .map_err(|e| Error::new(ErrorKind::NoAuthenticatingAuthority, format!("{e:?}"))) + .map_err(|e| custom_err!("failed to send KDC request over TCP", e))?; + + Ok(buf) + } + + async fn send_udp(&self, url: &Url, data: &[u8]) -> ConnectorResult> { + let udp_socket = UdpSocket::bind((IpAddr::V4(Ipv4Addr::LOCALHOST), 0)) + .await + .map_err(|e| custom_err!("cannot bind UDP socket", e))?; + + let addr = format!("{}:{}", url.host_str().unwrap_or_default(), url.port().unwrap_or(88)); + + udp_socket + .send_to(data, addr) + .await + .map_err(|e| custom_err!("failed to send UDP request", e))?; + + // 48 000 bytes: default maximum token len in Windows + let mut buf = vec![0; 0xbb80]; + + let n = udp_socket + .recv(&mut buf) + .await + .map_err(|e| custom_err!("failed to receive UDP request", e))?; + let buf = &buf[0..n]; + + let mut reply_buf = Vec::with_capacity(n + 4); + let n = u32::try_from(n).map_err(|e| custom_err!("invalid length", e))?; + reply_buf.extend_from_slice(&n.to_be_bytes()); + reply_buf.extend_from_slice(buf); + + Ok(reply_buf) + } + + async fn send_http(&mut self, url: &Url, data: &[u8]) -> ConnectorResult> { + let client = self.client.get_or_insert_with(Client::new); + + let response = client + .post(url.clone()) + .body(data.to_vec()) + .send() + .await + .map_err(|e| custom_err!("failed to send KDC request over proxy", e))? + .error_for_status() + .map_err(|e| custom_err!("KdcProxy", e))?; + + let body = response + .bytes() + .await + .map_err(|e| custom_err!("failed to receive KDC response", e))?; + + // The type bytes::Bytes has a special From implementation for Vec. + let body = Vec::from(body); + + Ok(body) + } +} + +impl Default for ReqwestNetworkClient { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/ironrdp-web/.gitignore b/crates/ironrdp-web/.gitignore new file mode 100644 index 00000000..572d527f --- /dev/null +++ b/crates/ironrdp-web/.gitignore @@ -0,0 +1,4 @@ +/target +/bin +/pkg +/wasm-pack.log diff --git a/crates/ironrdp-web/Cargo.toml b/crates/ironrdp-web/Cargo.toml new file mode 100644 index 00000000..e9905553 --- /dev/null +++ b/crates/ironrdp-web/Cargo.toml @@ -0,0 +1,84 @@ +[package] +name = "ironrdp-web" +version = "0.0.0" +readme = "README.md" +description = "WebAssembly high-level bindings targeting web browsers" +publish = false +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false +crate-type = ["cdylib", "rlib"] + +[features] +default = ["panic_hook"] +panic_hook = ["iron-remote-desktop/panic_hook"] +qoi = ["ironrdp/qoi"] +qoiz = ["ironrdp/qoiz"] + +[dependencies] +# Protocols +ironrdp = { path = "../ironrdp", features = [ + "connector", + "session", + "input", + "graphics", + "dvc", + "cliprdr", + "svc", + "displaycontrol", + "pdu", +] } +ironrdp-core.path = "../ironrdp-core" +ironrdp-cliprdr-format.path = "../ironrdp-cliprdr-format" +ironrdp-futures.path = "../ironrdp-futures" +ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" +ironrdp-propertyset.path = "../ironrdp-propertyset" +ironrdp-rdpfile.path = "../ironrdp-rdpfile" +iron-remote-desktop.path = "../iron-remote-desktop" + +# WASM +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +web-sys = { version = "0.3", features = ["HtmlCanvasElement"] } +js-sys = "0.3" +gloo-net = { version = "0.6", default-features = false, features = ["websocket", "http", "io-util"] } +gloo-timers = { version = "0.3", default-features = false, features = ["futures"] } + +# Rendering +softbuffer = { version = "0.4", default-features = false } +png = "0.18" +resize = { version = "0.8", features = ["std"], default-features = false } +rgb = "0.8" + +# Enable WebAssembly support for a few crates +getrandom2 = { package = "getrandom", version = "0.2", features = ["js"] } +getrandom = { version = "0.3", features = ["wasm_js"] } +chrono = { version = "0.4", features = ["wasmbind"] } +time = { version = "0.3", features = ["wasm-bindgen"] } + +# Async +futures-util = { version = "0.3", features = ["sink", "io"] } +futures-channel = "0.3" + +# Logging +tracing = { version = "0.1", features = ["log"] } + +# Utils +anyhow = "1" +smallvec = "1.15" +x509-cert = { version = "0.2", default-features = false, features = ["std"] } +tap = "1" +semver = "1" +url = "2.5" +base64 = "0.22" + +[lints] +workspace = true diff --git a/crates/ironrdp-web/README.md b/crates/ironrdp-web/README.md new file mode 100644 index 00000000..a619c2d9 --- /dev/null +++ b/crates/ironrdp-web/README.md @@ -0,0 +1,11 @@ +# WASM bindings for web + +## 🛠️ Build with `wasm-pack build` + +``` +wasm-pack build +``` + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp-web/rust-toolchain.toml b/crates/ironrdp-web/rust-toolchain.toml new file mode 100644 index 00000000..962290a7 --- /dev/null +++ b/crates/ironrdp-web/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +targets = ["wasm32-unknown-unknown"] +profile = "minimal" diff --git a/crates/ironrdp-web/src/canvas.rs b/crates/ironrdp-web/src/canvas.rs new file mode 100644 index 00000000..96b9df50 --- /dev/null +++ b/crates/ironrdp-web/src/canvas.rs @@ -0,0 +1,93 @@ +use core::num::NonZeroU32; + +use anyhow::Context as _; +use ironrdp::pdu::geometry::{InclusiveRectangle, Rectangle as _}; +use softbuffer::{NoDisplayHandle, NoWindowHandle}; +use web_sys::HtmlCanvasElement; + +pub(crate) struct Canvas { + width: NonZeroU32, + surface: softbuffer::Surface, +} + +impl Canvas { + pub(crate) fn new(render_canvas: HtmlCanvasElement, width: NonZeroU32, height: NonZeroU32) -> anyhow::Result { + render_canvas.set_width(width.get()); + render_canvas.set_height(height.get()); + + #[cfg(target_arch = "wasm32")] + let mut surface = { + use softbuffer::SurfaceExtWeb as _; + softbuffer::Surface::from_canvas(render_canvas).expect("surface") + }; + + #[cfg(not(target_arch = "wasm32"))] + let mut surface = { + fn stub(_: HtmlCanvasElement) -> softbuffer::Surface { + unimplemented!() + } + + stub(render_canvas) + }; + + surface.resize(width, height).expect("surface resize"); + + Ok(Self { width, surface }) + } + + pub(crate) fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) { + self.surface.resize(width, height).expect("surface resize"); + self.width = width; + } + + pub(crate) fn draw(&mut self, buffer: &[u8], region: InclusiveRectangle) -> anyhow::Result<()> { + let region_width = region.width(); + let region_height = region.height(); + + let mut src = buffer.chunks_exact(4).map(|pixel| { + let r = pixel[0]; + let g = pixel[1]; + let b = pixel[2]; + u32::from_be_bytes([0, r, g, b]) + }); + + let mut dst = self.surface.buffer_mut().expect("surface buffer"); + + { + // Copy src into dst + + let region_top_usize = usize::from(region.top); + let region_height_usize = usize::from(region_height); + let region_left_usize = usize::from(region.left); + let region_width_usize = usize::from(region_width); + + for dst_row in dst + .chunks_exact_mut(usize::try_from(self.width.get()).context("canvas width")?) + .skip(region_top_usize) + .take(region_height_usize) + { + let src_row = src.by_ref().take(region_width_usize); + + dst_row + .iter_mut() + .skip(region_left_usize) + .take(region_width_usize) + .zip(src_row) + .for_each(|(dst, src)| *dst = src); + } + } + + let damage_rect = softbuffer::Rect { + x: u32::from(region.left), + y: u32::from(region.top), + width: NonZeroU32::new(u32::from(region_width)) + .expect("per InclusiveRectangle invariants: 0 < region_width"), + height: NonZeroU32::new(u32::from(region_height)) + .expect("per InclusiveRectangle invariants: 0 < region_height"), + }; + + dst.present_with_damage(&[damage_rect]).expect("buffer present"); + + Ok(()) + } +} diff --git a/crates/ironrdp-web/src/clipboard.rs b/crates/ironrdp-web/src/clipboard.rs new file mode 100644 index 00000000..3f2e91e5 --- /dev/null +++ b/crates/ironrdp-web/src/clipboard.rs @@ -0,0 +1,674 @@ +//! This module implements browser-based clipboard backend for CLIPRDR SVC +//! +//! # Implementation notes +//! +//! A catch with web browsers, is that there is no support for delayed clipboard rendering. +//! We can’t know which format will be requested by the target application ultimately. +//! Because of that, we need to fetch optimistically all available formats. +//! +//! For instance, we query both "text/plain" and "text/html". Indeed, depending on the +//! target application in which the user performs the paste operation, either one could be +//! requested: when pasting into notepad, which does not support "text/html", "text/plain" +//! will be requested, and when pasting into WordPad, "text/html" will be requested. + +use std::collections::HashMap; + +use futures_channel::mpsc; +use iron_remote_desktop::ClipboardData as _; +use ironrdp::cliprdr::backend::{ClipboardMessage, CliprdrBackend}; +use ironrdp::cliprdr::pdu::{ + ClipboardFormat, ClipboardFormatId, ClipboardFormatName, ClipboardGeneralCapabilityFlags, FileContentsRequest, + FileContentsResponse, FormatDataRequest, FormatDataResponse, LockDataId, +}; +use ironrdp_cliprdr_format::bitmap::{dib_to_png, dibv5_to_png, png_to_cf_dibv5}; +use ironrdp_cliprdr_format::html::{cf_html_to_plain_html, plain_html_to_cf_html}; +use ironrdp_core::{impl_as_any, IntoOwned as _}; +use tracing::{error, trace, warn}; +use wasm_bindgen::prelude::*; + +use crate::session::RdpInputEvent; + +const MIME_TEXT: &str = "text/plain"; +const MIME_HTML: &str = "text/html"; +const MIME_PNG: &str = "image/png"; + +#[derive(Clone, Copy)] +struct ClientFormatDescriptor { + id: ClipboardFormatId, + name: &'static str, +} + +impl ClientFormatDescriptor { + const fn new(id: ClipboardFormatId, name: &'static str) -> Self { + Self { id, name } + } +} + +impl From for ClipboardFormat { + fn from(descriptor: ClientFormatDescriptor) -> Self { + ClipboardFormat::new(descriptor.id).with_name(ClipboardFormatName::new_static(descriptor.name)) + } +} + +const FORMAT_WIN_HTML_ID: ClipboardFormatId = ClipboardFormatId(0xC001); +const FORMAT_MIME_HTML_ID: ClipboardFormatId = ClipboardFormatId(0xC002); +const FORMAT_PNG_ID: ClipboardFormatId = ClipboardFormatId(0xC003); +const FORMAT_MIME_PNG_ID: ClipboardFormatId = ClipboardFormatId(0xC004); + +const FORMAT_WIN_HTML_NAME: &str = "HTML Format"; +const FORMAT_MIME_HTML_NAME: &str = "text/html"; +const FORMAT_PNG_NAME: &str = "PNG"; +const FORMAT_MIME_PNG_NAME: &str = "image/png"; + +const FORMAT_WIN_HTML: ClientFormatDescriptor = ClientFormatDescriptor::new(FORMAT_WIN_HTML_ID, FORMAT_WIN_HTML_NAME); +const FORMAT_MIME_HTML: ClientFormatDescriptor = + ClientFormatDescriptor::new(FORMAT_MIME_HTML_ID, FORMAT_MIME_HTML_NAME); +const FORMAT_PNG: ClientFormatDescriptor = ClientFormatDescriptor::new(FORMAT_PNG_ID, FORMAT_PNG_NAME); +const FORMAT_MIME_PNG: ClientFormatDescriptor = ClientFormatDescriptor::new(FORMAT_MIME_PNG_ID, FORMAT_MIME_PNG_NAME); + +/// Message proxy used to send clipboard-related messages to the application main event loop +#[derive(Debug, Clone)] +pub(crate) struct WasmClipboardMessageProxy { + tx: mpsc::UnboundedSender, +} + +impl WasmClipboardMessageProxy { + pub(crate) fn new(tx: mpsc::UnboundedSender) -> Self { + Self { tx } + } + + /// Send messages which require action on CLIPRDR SVC + pub(crate) fn send_cliprdr_message(&self, message: ClipboardMessage) { + if self.tx.unbounded_send(RdpInputEvent::Cliprdr(message)).is_err() { + error!("Failed to send os clipboard message, receiver is closed"); + } + } + + /// Send messages which require action on wasm clipboard backend + pub(crate) fn send_backend_message(&self, message: WasmClipboardBackendMessage) { + if self + .tx + .unbounded_send(RdpInputEvent::ClipboardBackend(message)) + .is_err() + { + error!("Failed to send os clipboard message, receiver is closed"); + } + } +} + +/// Messages sent by the JS code or CLIPRDR to the backend implementation. +#[derive(Debug)] +pub(crate) enum WasmClipboardBackendMessage { + LocalClipboardChanged(ClipboardData), + RemoteDataRequest(ClipboardFormatId), + + RemoteClipboardChanged(Vec), + RemoteDataResponse(FormatDataResponse<'static>), + + ForceClipboardUpdate, +} + +/// Clipboard backend implementation for web. This object should be created once per session and +/// kept alive until session is terminated. +pub(crate) struct WasmClipboard { + local_clipboard: Option, + remote_clipboard: ClipboardData, + + remote_mapping: HashMap, + remote_formats_to_read: Vec, + + proxy: WasmClipboardMessageProxy, + js_callbacks: JsClipboardCallbacks, +} + +/// Callbacks, required to interact with JS code from within the backend. +pub(crate) struct JsClipboardCallbacks { + pub(crate) on_remote_clipboard_changed: js_sys::Function, + pub(crate) on_force_clipboard_update: Option, +} + +impl WasmClipboard { + pub(crate) fn new(message_proxy: WasmClipboardMessageProxy, js_callbacks: JsClipboardCallbacks) -> Self { + Self { + local_clipboard: None, + remote_clipboard: ClipboardData::new(), + proxy: message_proxy, + js_callbacks, + + remote_mapping: HashMap::new(), + remote_formats_to_read: Vec::new(), + } + } + + /// Returns CLIPRDR backend implementation + pub(crate) fn backend(&self) -> WasmClipboardBackend { + WasmClipboardBackend { + proxy: self.proxy.clone(), + } + } + + fn handle_local_clipboard_changed( + &mut self, + clipboard_data: ClipboardData, + ) -> anyhow::Result> { + let mut formats = Vec::new(); + clipboard_data.items().iter().for_each(|item| { + match item.mime_type.as_str() { + MIME_TEXT => formats.push(ClipboardFormat::new(ClipboardFormatId::CF_UNICODETEXT)), + MIME_HTML => { + formats.extend([ + // We don't provide CF_TEXT, because it could be synthesized from + // CF_UNICODETEXT on the remote side. + ClipboardFormat::new(ClipboardFormatId::CF_UNICODETEXT), + FORMAT_WIN_HTML.into(), + FORMAT_MIME_HTML.into(), + ]); + } + MIME_PNG => { + formats.extend([ + // We don't provide CF_DIB, because it could be synthesized from + // CF_DIBV5 on the remote side. + ClipboardFormat::new(ClipboardFormatId::CF_DIBV5), + FORMAT_PNG.into(), + FORMAT_MIME_PNG.into(), + ]); + } + _ => {} + }; + }); + + self.local_clipboard = Some(clipboard_data); + + trace!("Sending clipboard formats: {:?}", formats); + + Ok(formats) + } + + fn process_remote_data_request( + &mut self, + format: ClipboardFormatId, + ) -> anyhow::Result> { + // Transaction is not set, bail! + let clipboard_data = if let Some(clipboard_data) = &self.local_clipboard { + clipboard_data + } else { + anyhow::bail!("Local clipboard is empty"); + }; + + let find_content_by_mime = |mime: &str| { + clipboard_data + .items() + .iter() + .find(|item| item.mime_type.as_str() == mime) + }; + + let find_text_content_by_mime = |mime: &str| { + find_content_by_mime(mime) + .and_then(|item| { + if let ClipboardItemValue::Text(text) = &item.value { + Some(text.as_str()) + } else { + None + } + }) + .ok_or_else(|| anyhow::anyhow!("Failed to find `{mime}` in client clipboard")) + }; + + let find_binary_content_by_mime = |mime: &str| { + find_content_by_mime(mime) + .and_then(|item| { + if let ClipboardItemValue::Binary(binary) = &item.value { + Some(binary.as_slice()) + } else { + None + } + }) + .ok_or_else(|| anyhow::anyhow!("Failed to find `{mime}` in client clipboard")) + }; + + let response = match format { + ClipboardFormatId::CF_UNICODETEXT => { + let text = find_text_content_by_mime(MIME_TEXT)?; + FormatDataResponse::new_unicode_string(text) + } + FORMAT_WIN_HTML_ID => { + let html_text = find_text_content_by_mime(MIME_HTML)?; + let cf_html = plain_html_to_cf_html(html_text); + FormatDataResponse::new_data(cf_html.into_bytes()) + } + FORMAT_MIME_HTML_ID => { + let html_text = find_text_content_by_mime(MIME_HTML)?; + FormatDataResponse::new_string(html_text) + } + ClipboardFormatId::CF_DIBV5 => { + let png_data = find_binary_content_by_mime(MIME_PNG)?; + let buffer = png_to_cf_dibv5(png_data)?; + FormatDataResponse::new_data(buffer) + } + FORMAT_MIME_PNG_ID | FORMAT_PNG_ID => { + let png_data = find_binary_content_by_mime(MIME_PNG)?; + FormatDataResponse::new_data(png_data) + } + _ => { + anyhow::bail!("Unknown format id requested: {}", format.value()); + } + }; + + Ok(response.into_owned()) + } + + fn process_remote_clipboard_changed( + &mut self, + formats: Vec, + ) -> anyhow::Result> { + self.remote_clipboard.clear(); + + // We accumulate all formats in the `remote_formats_to_read` attribute. + // Later, we loop over and fetch all of these (see `process_remote_data_response`). + self.remote_formats_to_read.clear(); + + // In this loop, we ignore some formats. There are two reasons for that: + // + // 1) Some formats require an extra conversion into the appropriate MIME format + // prior to being written to the system clipboard. + // E.g.: "image/png" format is preferred over "CF_DIB" because we’ll convert the + // uncompressed BMP into "image/png". "text/html" is preferred over Windows + // "CF_HTML" because we’ll convert it into "text/html". + // + // 2) A direct consequence of 1) is that some formats will end up being mapped + // into the same MIME type. Fetching only one of these is enough, especially given + // that delayed rendering is not an option. + for format in &formats { + if format.id().is_registered() { + if let Some(name) = format.name() { + const SUPPORTED_FORMATS: &[&str] = &[ + FORMAT_WIN_HTML.name, + FORMAT_MIME_HTML.name, + FORMAT_PNG.name, + FORMAT_MIME_PNG.name, + ]; + + if !SUPPORTED_FORMATS.iter().any(|supported| *supported == name.value()) { + // Unknown format + continue; + } + + let skip_win_html = format_name_eq(format, FORMAT_WIN_HTML.name) + && formats + .iter() + .any(|format| format_name_eq(format, FORMAT_MIME_HTML.name)); + + let skip_mime_png = format_name_eq(format, FORMAT_MIME_PNG.name) + && formats.iter().any(|format| format_name_eq(format, FORMAT_PNG.name)); + + if skip_win_html || skip_mime_png { + continue; + } + + self.remote_mapping.insert(format.id(), name.value().to_owned()); + } + } else { + const SUPPORTED_FORMATS: &[ClipboardFormatId] = &[ + ClipboardFormatId::CF_UNICODETEXT, + ClipboardFormatId::CF_DIB, + ClipboardFormatId::CF_DIBV5, + ]; + + if !SUPPORTED_FORMATS.contains(&format.id()) { + // Unknown format + continue; + } + + let skip_dib = format.id() == ClipboardFormatId::CF_DIB + && formats.iter().any(|format| { + format.id() == ClipboardFormatId::CF_DIBV5 + || format_name_eq(format, FORMAT_MIME_PNG.name) + || format_name_eq(format, FORMAT_PNG.name) + }); + + let skip_dibv5 = format.id() == ClipboardFormatId::CF_DIBV5 + && formats.iter().any(|format| { + format_name_eq(format, FORMAT_MIME_PNG.name) || format_name_eq(format, FORMAT_PNG.name) + }); + + if skip_dib || skip_dibv5 { + continue; + } + } + + self.remote_formats_to_read.push(format.id()); + } + + return Ok(self.remote_formats_to_read.last().copied()); + + fn format_name_eq(format: &ClipboardFormat, name: &str) -> bool { + format + .name() + .map(|actual: &ClipboardFormatName| actual.value() == name) + .unwrap_or(false) + } + } + + fn process_remote_data_response(&mut self, response: FormatDataResponse<'_>) -> anyhow::Result<()> { + let pending_format = match self.remote_formats_to_read.pop() { + Some(format) => format, + None => { + warn!("Remote returned format data, but no formats were requested"); + return Ok(()); + } + }; + + if response.is_error() { + // Format is not available anymore. + return Ok(()); + } + + let item = match pending_format { + ClipboardFormatId::CF_UNICODETEXT => match response.to_unicode_string() { + Ok(text) => Some(ClipboardItem::new_text(MIME_TEXT, text)), + Err(err) => { + error!("CF_UNICODETEXT decode error: {}", err); + None + } + }, + ClipboardFormatId::CF_DIB => match dib_to_png(response.data()) { + Ok(png) => Some(ClipboardItem::new_binary(MIME_PNG, png)), + Err(err) => { + warn!("DIB decode error: {}", err); + None + } + }, + ClipboardFormatId::CF_DIBV5 => match dibv5_to_png(response.data()) { + Ok(png) => Some(ClipboardItem::new_binary(MIME_PNG, png)), + Err(err) => { + warn!("DIBv5 decode error: {}", err); + None + } + }, + registered => { + let format_name = self.remote_mapping.get(®istered).map(|s| s.as_str()); + + match format_name { + Some(FORMAT_WIN_HTML_NAME) => match cf_html_to_plain_html(response.data()) { + Ok(text) => Some(ClipboardItem::new_text(MIME_HTML, text.to_owned())), + Err(err) => { + warn!("CF_HTML decode error: {}", err); + None + } + }, + Some(FORMAT_MIME_HTML_NAME) => match response.to_string() { + Ok(text) => Some(ClipboardItem::new_text(MIME_HTML, text)), + Err(err) => { + warn!("text/html decode error: {}", err); + None + } + }, + Some(FORMAT_MIME_PNG_NAME) | Some(FORMAT_PNG_NAME) => { + Some(ClipboardItem::new_binary(MIME_PNG, response.data().to_owned())) + } + _ => { + // Not supported format + None + } + } + } + }; + + if let Some(item) = item { + self.remote_clipboard.add(item); + } + + if let Some(format) = self.remote_formats_to_read.last() { + // Request next format. + self.proxy + .send_cliprdr_message(ClipboardMessage::SendInitiatePaste(*format)); + } else { + // All formats were read, send clipboard to JS. + let clipboard_data = core::mem::take(&mut self.remote_clipboard); + + if clipboard_data.is_empty() { + return Ok(()); + } + + // Set clipboard when all formats were read. + self.js_callbacks + .on_remote_clipboard_changed + .call1( + &JsValue::NULL, + &JsValue::from(crate::wasm_bridge::ClipboardData::from(clipboard_data)), + ) + .expect("failed to call JS callback"); + } + + Ok(()) + } + + /// Process backend event. This method should be called from the main event loop. + pub(crate) fn process_event(&mut self, event: WasmClipboardBackendMessage) -> anyhow::Result<()> { + match event { + WasmClipboardBackendMessage::LocalClipboardChanged(clipboard_data) => { + match self.handle_local_clipboard_changed(clipboard_data) { + Ok(formats) => { + self.proxy + .send_cliprdr_message(ClipboardMessage::SendInitiateCopy(formats)); + } + Err(e) => { + // Not a critical error, we could skip single clipboard update. + error!(error = format!("{e:#}"), "Failed to handle local clipboard change"); + } + } + } + WasmClipboardBackendMessage::RemoteDataRequest(format) => { + let message = match self.process_remote_data_request(format) { + Ok(message) => message, + Err(e) => { + // Not a critical error, but we should notify remote about it. + error!(error = format!("{e:#}"), "Failed to process remote data request"); + FormatDataResponse::new_error() + } + }; + self.proxy + .send_cliprdr_message(ClipboardMessage::SendFormatData(message)); + } + WasmClipboardBackendMessage::RemoteClipboardChanged(formats) => { + match self.process_remote_clipboard_changed(formats) { + Ok(Some(format)) => { + // We start querying formats right away. This is due absence of + // delay-rendering in web client. + self.proxy + .send_cliprdr_message(ClipboardMessage::SendInitiatePaste(format)); + } + Ok(None) => { + // No formats to query + } + Err(e) => { + error!(error = format!("{e:#}"), "Failed to process remote clipboard change"); + } + } + } + WasmClipboardBackendMessage::RemoteDataResponse(formats) => { + match self.process_remote_data_response(formats) { + Ok(()) => {} + Err(e) => { + error!(error = format!("{e:#}"), "Failed to process remote data response"); + } + } + } + WasmClipboardBackendMessage::ForceClipboardUpdate => { + if let Some(callback) = self.js_callbacks.on_force_clipboard_update.as_mut() { + callback.call0(&JsValue::NULL).expect("failed to call JS callback"); + } else { + // If no initial clipboard callback was set, send empty format list instead + return self + .process_event(WasmClipboardBackendMessage::LocalClipboardChanged(ClipboardData::new())); + } + } + }; + + Ok(()) + } +} + +/// CLIPRDR backend implementation for web. This object could be instantiated via [`WasmClipboard`] +/// to pass it to CLIPRDR SVC constructor. +#[derive(Debug)] +pub(crate) struct WasmClipboardBackend { + proxy: WasmClipboardMessageProxy, +} + +impl WasmClipboardBackend { + fn send_event(&self, event: WasmClipboardBackendMessage) { + self.proxy.send_backend_message(event); + } +} + +impl_as_any!(WasmClipboardBackend); + +impl CliprdrBackend for WasmClipboardBackend { + fn temporary_directory(&self) -> &str { + ".cliprdr" + } + + fn client_capabilities(&self) -> ClipboardGeneralCapabilityFlags { + // No additional capabilities yet + ClipboardGeneralCapabilityFlags::empty() + } + + fn on_ready(&mut self) {} + + fn on_request_format_list(&mut self) { + // Initial clipboard is assumed to be empty on WASM (TODO: This is only relevant for Firefox?) + self.send_event(WasmClipboardBackendMessage::ForceClipboardUpdate); + } + + fn on_process_negotiated_capabilities(&mut self, _: ClipboardGeneralCapabilityFlags) { + // No additional capabilities yet + } + + fn on_remote_copy(&mut self, available_formats: &[ClipboardFormat]) { + self.send_event(WasmClipboardBackendMessage::RemoteClipboardChanged( + available_formats.to_vec(), + )); + } + + fn on_format_data_request(&mut self, request: FormatDataRequest) { + self.send_event(WasmClipboardBackendMessage::RemoteDataRequest(request.format)); + } + + fn on_format_data_response(&mut self, response: FormatDataResponse<'_>) { + self.send_event(WasmClipboardBackendMessage::RemoteDataResponse(response.into_owned())); + } + + fn on_file_contents_request(&mut self, _request: FileContentsRequest) { + // File transfer not implemented yet + } + + fn on_file_contents_response(&mut self, _response: FileContentsResponse<'_>) { + // File transfer not implemented yet + } + + fn on_lock(&mut self, _data_id: LockDataId) { + // File transfer not implemented yet + } + + fn on_unlock(&mut self, _data_id: LockDataId) { + // File transfer not implemented yet + } +} + +/// Object which represents complete clipboard transaction with multiple MIME types. +#[derive(Debug, Default, Clone)] +pub(crate) struct ClipboardData { + items: Vec, +} + +impl ClipboardData { + pub(crate) fn new() -> Self { + Self { items: Vec::new() } + } + + pub(crate) fn add(&mut self, item: ClipboardItem) { + self.items.push(item); + } + + pub(crate) fn clear(&mut self) { + self.items.clear(); + } +} + +impl iron_remote_desktop::ClipboardData for ClipboardData { + type Item = ClipboardItem; + + fn create() -> Self { + Self::new() + } + + fn add_text(&mut self, mime_type: &str, text: &str) { + self.items.push(ClipboardItem { + mime_type: mime_type.to_owned(), + value: ClipboardItemValue::Text(text.to_owned()), + }) + } + + fn add_binary(&mut self, mime_type: &str, binary: &[u8]) { + self.items.push(ClipboardItem { + mime_type: mime_type.to_owned(), + value: ClipboardItemValue::Binary(binary.to_owned()), + }) + } + + fn items(&self) -> &[Self::Item] { + &self.items + } +} + +impl FromIterator for ClipboardData { + fn from_iter>(iter: T) -> Self { + Self { + items: iter.into_iter().collect(), + } + } +} + +#[derive(Debug, Clone)] +pub(crate) enum ClipboardItemValue { + Text(String), + Binary(Vec), +} + +/// Object which represents single clipboard format represented standard MIME type. +#[derive(Debug, Clone)] +pub(crate) struct ClipboardItem { + mime_type: String, + value: ClipboardItemValue, +} + +impl ClipboardItem { + pub(crate) fn new_text(mime_type: impl Into, text: String) -> Self { + Self { + mime_type: mime_type.into(), + value: ClipboardItemValue::Text(text), + } + } + + pub(crate) fn new_binary(mime_type: impl Into, payload: Vec) -> Self { + Self { + mime_type: mime_type.into(), + value: ClipboardItemValue::Binary(payload), + } + } +} + +impl iron_remote_desktop::ClipboardItem for ClipboardItem { + fn mime_type(&self) -> &str { + &self.mime_type + } + + #[expect(refining_impl_trait)] + fn value(&self) -> JsValue { + match &self.value { + ClipboardItemValue::Text(text) => JsValue::from_str(text), + ClipboardItemValue::Binary(binary) => JsValue::from(js_sys::Uint8Array::from(binary.as_slice())), + } + } +} diff --git a/crates/ironrdp-web/src/error.rs b/crates/ironrdp-web/src/error.rs new file mode 100644 index 00000000..93772856 --- /dev/null +++ b/crates/ironrdp-web/src/error.rs @@ -0,0 +1,67 @@ +use iron_remote_desktop::IronErrorKind; +use ironrdp::connector::{self, sspi, ConnectorErrorKind}; + +pub(crate) struct IronError { + kind: IronErrorKind, + source: anyhow::Error, +} + +impl IronError { + pub(crate) fn with_kind(mut self, kind: IronErrorKind) -> Self { + self.kind = kind; + self + } +} + +impl iron_remote_desktop::IronError for IronError { + fn backtrace(&self) -> String { + format!("{:#}", self.source) + } + + fn kind(&self) -> IronErrorKind { + self.kind + } +} + +impl From for IronError { + fn from(e: connector::ConnectorError) -> Self { + use sspi::credssp::NStatusCode; + + let kind = match e.kind() { + ConnectorErrorKind::Credssp(sspi::Error { + nstatus: Some(NStatusCode::WRONG_PASSWORD), + .. + }) => IronErrorKind::WrongPassword, + ConnectorErrorKind::Credssp(sspi::Error { + nstatus: Some(NStatusCode::LOGON_FAILURE), + .. + }) => IronErrorKind::LogonFailure, + ConnectorErrorKind::AccessDenied => IronErrorKind::AccessDenied, + ConnectorErrorKind::Negotiation(_) => IronErrorKind::NegotiationFailure, + _ => IronErrorKind::General, + }; + + Self { + kind, + source: anyhow::Error::new(e), + } + } +} + +impl From for IronError { + fn from(e: ironrdp::session::SessionError) -> Self { + Self { + kind: IronErrorKind::General, + source: anyhow::Error::new(e), + } + } +} + +impl From for IronError { + fn from(e: anyhow::Error) -> Self { + Self { + kind: IronErrorKind::General, + source: e, + } + } +} diff --git a/crates/ironrdp-web/src/image.rs b/crates/ironrdp-web/src/image.rs new file mode 100644 index 00000000..13ac3fed --- /dev/null +++ b/crates/ironrdp-web/src/image.rs @@ -0,0 +1,73 @@ +#![allow(clippy::arithmetic_side_effects)] + +use ironrdp::pdu::geometry::{InclusiveRectangle, Rectangle as _}; +use ironrdp::session::image::DecodedImage; + +pub(crate) fn extract_partial_image(image: &DecodedImage, region: InclusiveRectangle) -> (InclusiveRectangle, Vec) { + // PERF: needs actual benchmark to find a better heuristic + if region.height() > 64 || region.width() > 512 { + extract_whole_rows(image, region) + } else { + extract_smallest_rectangle(image, region) + } +} + +// Faster for low-height and smaller images +fn extract_smallest_rectangle(image: &DecodedImage, region: InclusiveRectangle) -> (InclusiveRectangle, Vec) { + let pixel_size = usize::from(image.pixel_format().bytes_per_pixel()); + + let image_width = usize::from(image.width()); + let image_stride = image_width * pixel_size; + + let region_top = usize::from(region.top); + let region_left = usize::from(region.left); + let region_width = usize::from(region.width()); + let region_height = usize::from(region.height()); + let region_stride = region_width * pixel_size; + + let dst_buf_size = region_width * region_height * pixel_size; + let mut dst = vec![0; dst_buf_size]; + + let src = image.data(); + + for row in 0..region_height { + let src_begin = image_stride * (region_top + row) + region_left * pixel_size; + let src_end = src_begin + region_stride; + let src_slice = &src[src_begin..src_end]; + + let target_begin = region_stride * row; + let target_end = target_begin + region_stride; + let target_slice = &mut dst[target_begin..target_end]; + + target_slice.copy_from_slice(src_slice); + } + + (region, dst) +} + +// Faster for high-height and bigger images +fn extract_whole_rows(image: &DecodedImage, region: InclusiveRectangle) -> (InclusiveRectangle, Vec) { + let pixel_size = usize::from(image.pixel_format().bytes_per_pixel()); + + let image_width = usize::from(image.width()); + let image_stride = image_width * pixel_size; + + let region_top = usize::from(region.top); + let region_bottom = usize::from(region.bottom); + + let src = image.data(); + + let src_begin = region_top * image_stride; + let src_end = (region_bottom + 1) * image_stride; + + let dst = src[src_begin..src_end].to_vec(); + + let wider_region = InclusiveRectangle { + left: 0, + top: region.top, + right: image.width() - 1, + bottom: region.bottom, + }; + + (wider_region, dst) +} diff --git a/crates/ironrdp-web/src/input.rs b/crates/ironrdp-web/src/input.rs new file mode 100644 index 00000000..c6f1ea7e --- /dev/null +++ b/crates/ironrdp-web/src/input.rs @@ -0,0 +1,92 @@ +use iron_remote_desktop::RotationUnit; +use ironrdp::input::{MouseButton, MousePosition, Operation, Scancode, WheelRotations}; +use smallvec::SmallVec; +use tracing::warn; + +#[derive(Clone)] +pub(crate) struct DeviceEvent(Operation); + +impl iron_remote_desktop::DeviceEvent for DeviceEvent { + fn mouse_button_pressed(button: u8) -> Self { + match MouseButton::from_web_button(button) { + Some(button) => Self(Operation::MouseButtonPressed(button)), + None => { + warn!("Unknown mouse button ID: {button}"); + Self(Operation::MouseButtonPressed(MouseButton::Left)) + } + } + } + + fn mouse_button_released(button: u8) -> Self { + match MouseButton::from_web_button(button) { + Some(button) => Self(Operation::MouseButtonReleased(button)), + None => { + warn!("Unknown mouse button ID: {button}"); + Self(Operation::MouseButtonReleased(MouseButton::Left)) + } + } + } + + fn mouse_move(x: u16, y: u16) -> Self { + Self(Operation::MouseMove(MousePosition { x, y })) + } + + fn wheel_rotations(vertical: bool, rotation_amount: i16, rotation_unit: RotationUnit) -> Self { + const LINES_TO_PIXELS_SCALE: i16 = 50; + const PAGES_TO_LINES_SCALE: i16 = 38; + + let lines_to_pixels = |lines: i16| lines * LINES_TO_PIXELS_SCALE; + + let pages_to_pixels = |pages: i16| pages * PAGES_TO_LINES_SCALE * LINES_TO_PIXELS_SCALE; + + let rotation_amount = match rotation_unit { + RotationUnit::Pixel => rotation_amount, + RotationUnit::Line => lines_to_pixels(rotation_amount), + RotationUnit::Page => pages_to_pixels(rotation_amount), + }; + + Self(Operation::WheelRotations(WheelRotations { + is_vertical: vertical, + rotation_units: rotation_amount, + })) + } + + fn key_pressed(scancode: u16) -> Self { + Self(Operation::KeyPressed(Scancode::from_u16(scancode))) + } + + fn key_released(scancode: u16) -> Self { + Self(Operation::KeyReleased(Scancode::from_u16(scancode))) + } + + fn unicode_pressed(unicode: char) -> Self { + Self(Operation::UnicodeKeyPressed(unicode)) + } + + fn unicode_released(unicode: char) -> Self { + Self(Operation::UnicodeKeyReleased(unicode)) + } +} + +pub(crate) struct InputTransaction(SmallVec<[Operation; 3]>); + +impl iron_remote_desktop::InputTransaction for InputTransaction { + type DeviceEvent = DeviceEvent; + + fn create() -> Self { + Self(SmallVec::new()) + } + + fn add_event(&mut self, event: Self::DeviceEvent) { + self.0.push(event.0); + } +} + +impl IntoIterator for InputTransaction { + type IntoIter = smallvec::IntoIter<[Operation; 3]>; + type Item = Operation; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/crates/ironrdp-web/src/lib.rs b/crates/ironrdp-web/src/lib.rs new file mode 100644 index 00000000..b98ebb14 --- /dev/null +++ b/crates/ironrdp-web/src/lib.rs @@ -0,0 +1,47 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![cfg_attr( + doc, + doc( + html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg" + ) +)] +#![allow(clippy::new_without_default)] // Default trait can’t be used by wasm consumer anyway. + +// Silence the unused_crate_dependencies lint. +// These crates are added just to enable additional WASM features. +extern crate chrono as _; +extern crate getrandom as _; +extern crate getrandom2 as _; +extern crate time as _; + +mod canvas; +mod clipboard; +mod error; +mod image; +mod input; +mod network_client; +mod rdp_file; +mod session; + +mod wasm_bridge { + use tracing::debug; + + struct Api; + + impl iron_remote_desktop::RemoteDesktopApi for Api { + type Session = crate::session::Session; + type SessionBuilder = crate::session::SessionBuilder; + type SessionTerminationInfo = crate::session::SessionTerminationInfo; + type DeviceEvent = crate::input::DeviceEvent; + type InputTransaction = crate::input::InputTransaction; + type ClipboardData = crate::clipboard::ClipboardData; + type ClipboardItem = crate::clipboard::ClipboardItem; + type Error = crate::error::IronError; + + fn post_setup() { + debug!("IronRDP is ready"); + } + } + + iron_remote_desktop::make_bridge!(Api); +} diff --git a/crates/ironrdp-web/src/network_client.rs b/crates/ironrdp-web/src/network_client.rs new file mode 100644 index 00000000..0e32efe8 --- /dev/null +++ b/crates/ironrdp-web/src/network_client.rs @@ -0,0 +1,45 @@ +use ironrdp::connector::sspi::generator::NetworkRequest; +use ironrdp::connector::sspi::network_client::NetworkProtocol; +use ironrdp::connector::{custom_err, reason_err, ConnectorResult}; +use ironrdp_futures::NetworkClient; +use tracing::debug; + +#[derive(Debug)] +pub(crate) struct WasmNetworkClient; + +impl NetworkClient for WasmNetworkClient { + async fn send(&mut self, network_request: &NetworkRequest) -> ConnectorResult> { + debug!(?network_request.protocol, ?network_request.url); + + match &network_request.protocol { + NetworkProtocol::Http | NetworkProtocol::Https => { + let body = js_sys::Uint8Array::from(network_request.data.as_slice()); + + let response = gloo_net::http::Request::post(network_request.url.as_str()) + .header("keep-alive", "true") + .body(body) + .map_err(|e| custom_err!("failed to send KDC request", e))? + .send() + .await + .map_err(|e| custom_err!("failed to send KDC request", e))?; + + if !response.ok() { + return Err(reason_err!( + "KdcProxy", + "HTTP status error ({} {})", + response.status(), + response.status_text(), + )); + } + + let body = response + .binary() + .await + .map_err(|e| custom_err!("failed to retrieve HTTP response", e))?; + + Ok(body) + } + unsupported => Err(reason_err!("CredSSP", "unsupported protocol: {unsupported:?}")), + } + } +} diff --git a/crates/ironrdp-web/src/rdp_file.rs b/crates/ironrdp-web/src/rdp_file.rs new file mode 100644 index 00000000..bb154392 --- /dev/null +++ b/crates/ironrdp-web/src/rdp_file.rs @@ -0,0 +1,47 @@ +use tracing::error; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +pub struct RdpFile(ironrdp_propertyset::PropertySet); + +#[wasm_bindgen] +impl RdpFile { + #[wasm_bindgen(constructor)] + pub fn create() -> Self { + Self(ironrdp_propertyset::PropertySet::new()) + } + + pub fn parse(&mut self, config: &str) { + let parse_result = ironrdp_rdpfile::parse(config); + + self.0 = parse_result.properties; + + for e in parse_result.errors { + error!("Error when reading configuration: {e}"); + } + } + + pub fn write(&self) -> String { + ironrdp_rdpfile::write(&self.0) + } + + #[wasm_bindgen(js_name = "insertStr")] + pub fn insert_str(&mut self, key: String, value: &str) { + self.0.insert(key, value); + } + + #[wasm_bindgen(js_name = "insertInt")] + pub fn insert_int(&mut self, key: String, value: i32) { + self.0.insert(key, value); + } + + #[wasm_bindgen(js_name = "getStr")] + pub fn get_str(&self, key: &str) -> Option { + self.0.get::<&str>(key).map(|str| str.to_owned()) + } + + #[wasm_bindgen(js_name = "getInt")] + pub fn get_int(&self, key: &str) -> Option { + self.0.get::(key) + } +} diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs new file mode 100644 index 00000000..6d0e018a --- /dev/null +++ b/crates/ironrdp-web/src/session.rs @@ -0,0 +1,1159 @@ +use core::cell::RefCell; +use core::net::{Ipv4Addr, SocketAddrV4}; +use core::num::NonZeroU32; +use core::time::Duration; +use std::borrow::Cow; +use std::rc::Rc; + +use anyhow::Context as _; +use base64::Engine as _; +use futures_channel::mpsc; +use futures_util::io::{ReadHalf, WriteHalf}; +use futures_util::{select, AsyncWriteExt as _, FutureExt as _, StreamExt as _}; +use gloo_net::websocket; +use gloo_net::websocket::futures::WebSocket; +use iron_remote_desktop::{CursorStyle, DesktopSize, Extension, IronErrorKind}; +use ironrdp::cliprdr::backend::ClipboardMessage; +use ironrdp::cliprdr::CliprdrClient; +use ironrdp::connector::connection_activation::ConnectionActivationState; +use ironrdp::connector::credssp::KerberosConfig; +use ironrdp::connector::{self, ClientConnector, Credentials}; +use ironrdp::displaycontrol::client::DisplayControlClient; +use ironrdp::dvc::DrdynvcClient; +use ironrdp::graphics::image_processing::PixelFormat; +use ironrdp::pdu::input::fast_path::FastPathInputEvent; +use ironrdp::pdu::rdp::capability_sets::client_codecs_capabilities; +use ironrdp::pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; +use ironrdp::session::image::DecodedImage; +use ironrdp::session::{fast_path, ActiveStage, ActiveStageOutput, GracefulDisconnectReason}; +use ironrdp_core::WriteBuf; +use ironrdp_futures::{single_sequence_step_read, FramedWrite}; +use rgb::AsPixels as _; +use tap::prelude::*; +use tracing::{debug, error, info, trace, warn}; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::spawn_local; +use web_sys::HtmlCanvasElement; + +use crate::canvas::Canvas; +use crate::clipboard; +use crate::clipboard::{ClipboardData, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage}; +use crate::error::IronError; +use crate::image::extract_partial_image; +use crate::input::InputTransaction; +use crate::network_client::WasmNetworkClient; + +const DEFAULT_WIDTH: u16 = 1280; +const DEFAULT_HEIGHT: u16 = 720; + +#[derive(Clone, Default)] +pub(crate) struct SessionBuilder(Rc>); + +struct SessionBuilderInner { + username: Option, + destination: Option, + server_domain: Option, + password: Option, + proxy_address: Option, + auth_token: Option, + pcb: Option, + kdc_proxy_url: Option, + client_name: String, + desktop_size: DesktopSize, + + render_canvas: Option, + set_cursor_style_callback: Option, + set_cursor_style_callback_context: Option, + remote_clipboard_changed_callback: Option, + force_clipboard_update_callback: Option, + + use_display_control: bool, + enable_credssp: bool, + outbound_message_size_limit: Option, +} + +impl Default for SessionBuilderInner { + fn default() -> Self { + Self { + username: None, + destination: None, + server_domain: None, + password: None, + proxy_address: None, + auth_token: None, + pcb: None, + kdc_proxy_url: None, + client_name: "ironrdp-web".to_owned(), + desktop_size: DesktopSize { + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + }, + + render_canvas: None, + set_cursor_style_callback: None, + set_cursor_style_callback_context: None, + remote_clipboard_changed_callback: None, + force_clipboard_update_callback: None, + + use_display_control: false, + enable_credssp: true, + outbound_message_size_limit: None, + } + } +} + +impl iron_remote_desktop::SessionBuilder for SessionBuilder { + type Session = Session; + type Error = IronError; + + fn create() -> Self { + Self(Rc::new(RefCell::new(SessionBuilderInner::default()))) + } + + /// Required + fn username(&self, username: String) -> Self { + self.0.borrow_mut().username = Some(username); + self.clone() + } + + /// Required + fn destination(&self, destination: String) -> Self { + self.0.borrow_mut().destination = Some(destination); + self.clone() + } + + /// Optional + fn server_domain(&self, server_domain: String) -> Self { + self.0.borrow_mut().server_domain = if server_domain.is_empty() { + None + } else { + Some(server_domain) + }; + self.clone() + } + + /// Required + fn password(&self, password: String) -> Self { + self.0.borrow_mut().password = Some(password); + self.clone() + } + + /// Required + fn proxy_address(&self, address: String) -> Self { + self.0.borrow_mut().proxy_address = Some(address); + self.clone() + } + + /// Required + fn auth_token(&self, token: String) -> Self { + self.0.borrow_mut().auth_token = Some(token); + self.clone() + } + + /// Optional + fn desktop_size(&self, desktop_size: DesktopSize) -> Self { + self.0.borrow_mut().desktop_size = desktop_size; + self.clone() + } + + /// Optional + fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self { + self.0.borrow_mut().render_canvas = Some(canvas); + self.clone() + } + + /// Required. + /// + /// # Callback signature: + /// ```typescript + /// function callback( + /// cursor_kind: string, + /// cursor_data: string | undefined, + /// hotspot_x: number | undefined, + /// hotspot_y: number | undefined + /// ): void + /// ``` + /// + /// # Cursor kinds: + /// - `default` (default system cursor); other arguments are `UNDEFINED` + /// - `none` (hide cursor); other arguments are `UNDEFINED` + /// - `url` (custom cursor data URL); `cursor_data` contains the data URL with Base64-encoded + /// cursor bitmap; `hotspot_x` and `hotspot_y` are set to the cursor hotspot coordinates. + fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self { + self.0.borrow_mut().set_cursor_style_callback = Some(callback); + self.clone() + } + + /// Required. + fn set_cursor_style_callback_context(&self, context: JsValue) -> Self { + self.0.borrow_mut().set_cursor_style_callback_context = Some(context); + self.clone() + } + + /// Optional + fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> Self { + self.0.borrow_mut().remote_clipboard_changed_callback = Some(callback); + self.clone() + } + + /// Optional + fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self { + self.0.borrow_mut().force_clipboard_update_callback = Some(callback); + self.clone() + } + + /// Because the server does not resize the framebuffer in the RDP protocol, this feature is unused in IronRDP. + fn canvas_resized_callback(&self, _callback: js_sys::Function) -> Self { + self.clone() + } + + fn extension(&self, ext: Extension) -> Self { + iron_remote_desktop::extension_match! { + match ext; + |pcb: String| { self.0.borrow_mut().pcb = Some(pcb) }; + |kdc_proxy_url: String| { self.0.borrow_mut().kdc_proxy_url = Some(kdc_proxy_url) }; + |display_control: bool| { self.0.borrow_mut().use_display_control = display_control }; + |enable_credssp: bool| { self.0.borrow_mut().enable_credssp = enable_credssp }; + |outbound_message_size_limit: f64| { + let limit = if outbound_message_size_limit >= 0.0 && outbound_message_size_limit <= f64::from(u32::MAX) { + #[expect(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] + { outbound_message_size_limit as usize } + } else { + warn!(outbound_message_size_limit, "Invalid outbound message size limit; fallback to unlimited"); + 0 // Fallback to no limit for invalid values. + }; + self.0.borrow_mut().outbound_message_size_limit = if limit > 0 { Some(limit) } else { None }; + }; + } + + self.clone() + } + + async fn connect(&self) -> Result { + let ( + username, + destination, + server_domain, + password, + proxy_address, + auth_token, + pcb, + kdc_proxy_url, + client_name, + desktop_size, + render_canvas, + set_cursor_style_callback, + set_cursor_style_callback_context, + remote_clipboard_changed_callback, + force_clipboard_update_callback, + outbound_message_size_limit, + ); + + { + let inner = self.0.borrow(); + + username = inner.username.clone().context("username missing")?; + destination = inner.destination.clone().context("destination missing")?; + server_domain = inner.server_domain.clone(); + password = inner.password.clone().context("password missing")?; + proxy_address = inner.proxy_address.clone().context("proxy_address missing")?; + auth_token = inner.auth_token.clone().context("auth_token missing")?; + pcb = inner.pcb.clone(); + kdc_proxy_url = inner.kdc_proxy_url.clone(); + client_name = inner.client_name.clone(); + desktop_size = inner.desktop_size; + + render_canvas = inner.render_canvas.clone().context("render_canvas missing")?; + + set_cursor_style_callback = inner + .set_cursor_style_callback + .clone() + .context("set_cursor_style_callback missing")?; + set_cursor_style_callback_context = inner + .set_cursor_style_callback_context + .clone() + .context("set_cursor_style_callback_context missing")?; + remote_clipboard_changed_callback = inner.remote_clipboard_changed_callback.clone(); + force_clipboard_update_callback = inner.force_clipboard_update_callback.clone(); + outbound_message_size_limit = inner.outbound_message_size_limit; + } + + info!("Connect to RDP host"); + + let mut config = build_config(username, password, server_domain, client_name, desktop_size); + + let enable_credssp = self.0.borrow().enable_credssp; + config.enable_credssp = enable_credssp; + + let (input_events_tx, input_events_rx) = mpsc::unbounded(); + + let clipboard = remote_clipboard_changed_callback.clone().map(|callback| { + WasmClipboard::new( + clipboard::WasmClipboardMessageProxy::new(input_events_tx.clone()), + clipboard::JsClipboardCallbacks { + on_remote_clipboard_changed: callback, + on_force_clipboard_update: force_clipboard_update_callback, + }, + ) + }); + + let ws = WebSocket::open(&proxy_address).context("couldn't open WebSocket")?; + + // NOTE: ideally, when the WebSocket can't be opened, the above call should fail with details on why is that + // (e.g., the proxy hostname could not be resolved, proxy service is not running), but errors are neved + // bubbled up in practice, so instead we poll the WebSocket state until we know its connected (i.e., the + // WebSocket handshake is a success and user data can be exchanged). + loop { + match ws.state() { + websocket::State::Closing | websocket::State::Closed => { + return Err(IronError::from(anyhow::anyhow!( + "failed to connect to {proxy_address} (WebSocket is `{:?}`)", + ws.state() + )) + .with_kind(IronErrorKind::ProxyConnect)); + } + websocket::State::Connecting => { + trace!("WebSocket is connecting to proxy at {proxy_address}..."); + gloo_timers::future::sleep(Duration::from_millis(50)).await; + } + websocket::State::Open => { + debug!("WebSocket connected to {proxy_address} with success"); + break; + } + } + } + + let use_display_control = self.0.borrow().use_display_control; + + let (connection_result, ws) = connect(ConnectParams { + ws, + config, + proxy_auth_token: auth_token, + destination, + pcb, + kdc_proxy_url, + clipboard_backend: clipboard.as_ref().map(|clip| clip.backend()), + use_display_control, + }) + .await?; + + info!("Connected!"); + + let (rdp_reader, rdp_writer) = futures_util::AsyncReadExt::split(ws); + + let (writer_tx, writer_rx) = mpsc::unbounded(); + + spawn_local(writer_task(writer_rx, rdp_writer, outbound_message_size_limit)); + + Ok(Session { + desktop_size: connection_result.desktop_size, + input_database: RefCell::new(ironrdp::input::Database::new()), + writer_tx, + input_events_tx, + + render_canvas, + set_cursor_style_callback, + set_cursor_style_callback_context, + + input_events_rx: RefCell::new(Some(input_events_rx)), + rdp_reader: RefCell::new(Some(rdp_reader)), + connection_result: RefCell::new(Some(connection_result)), + clipboard: RefCell::new(Some(clipboard)), + }) + } +} + +pub(crate) type FastPathInputEvents = smallvec::SmallVec<[FastPathInputEvent; 2]>; + +#[derive(Debug)] +pub(crate) enum RdpInputEvent { + Cliprdr(ClipboardMessage), + ClipboardBackend(WasmClipboardBackendMessage), + FastPath(FastPathInputEvents), + Resize { + width: u32, + height: u32, + scale_factor: Option, + physical_size: Option<(u32, u32)>, + }, + TerminateSession, +} + +pub(crate) struct SessionTerminationInfo { + reason: GracefulDisconnectReason, +} + +impl iron_remote_desktop::SessionTerminationInfo for SessionTerminationInfo { + fn reason(&self) -> String { + self.reason.to_string() + } +} + +pub(crate) struct Session { + desktop_size: connector::DesktopSize, + input_database: RefCell, + writer_tx: mpsc::UnboundedSender>, + input_events_tx: mpsc::UnboundedSender, + + render_canvas: HtmlCanvasElement, + set_cursor_style_callback: js_sys::Function, + set_cursor_style_callback_context: JsValue, + + // Consumed when `run` is called + input_events_rx: RefCell>>, + connection_result: RefCell>, + rdp_reader: RefCell>>, + clipboard: RefCell>>, +} + +impl Session { + fn h_send_inputs(&self, inputs: smallvec::SmallVec<[FastPathInputEvent; 2]>) -> Result<(), IronError> { + if !inputs.is_empty() { + trace!("Inputs: {inputs:?}"); + + self.input_events_tx + .unbounded_send(RdpInputEvent::FastPath(inputs)) + .context("Send input events to writer task")?; + } + + Ok(()) + } + + fn set_cursor_style(&self, style: CursorStyle) -> Result<(), IronError> { + let (kind, data, hotspot_x, hotspot_y) = match style { + CursorStyle::Default => ("default", None, None, None), + CursorStyle::Hidden => ("hidden", None, None, None), + CursorStyle::Url { + data, + hotspot_x, + hotspot_y, + } => ("url", Some(data), Some(hotspot_x), Some(hotspot_y)), + }; + + let args = js_sys::Array::from_iter([ + JsValue::from_str(kind), + JsValue::from(data), + JsValue::from_f64(hotspot_x.unwrap_or_default().into()), + JsValue::from_f64(hotspot_y.unwrap_or_default().into()), + ]); + + let _ret = self + .set_cursor_style_callback + .apply(&self.set_cursor_style_callback_context, &args) + .map_err(|e| anyhow::Error::msg(format!("set cursor style callback failed: {e:?}")))?; + + Ok(()) + } +} + +impl iron_remote_desktop::Session for Session { + type SessionTerminationInfo = SessionTerminationInfo; + type InputTransaction = InputTransaction; + type ClipboardData = ClipboardData; + type Error = IronError; + + async fn run(&self) -> Result { + let rdp_reader = self + .rdp_reader + .borrow_mut() + .take() + .context("RDP session can be started only once")?; + + let mut input_events = self + .input_events_rx + .borrow_mut() + .take() + .context("RDP session can be started only once")?; + + let connection_result = self + .connection_result + .borrow_mut() + .take() + .expect("run called only once"); + + let mut clipboard = self.clipboard.borrow_mut().take().expect("run called only once"); + + let mut framed = ironrdp_futures::LocalFuturesFramed::new(rdp_reader); + + debug!("Initialize canvas"); + + let desktop_width = + NonZeroU32::new(u32::from(connection_result.desktop_size.width)).context("desktop width is zero")?; + let desktop_height = + NonZeroU32::new(u32::from(connection_result.desktop_size.height)).context("desktop height is zero")?; + + let mut gui = + Canvas::new(self.render_canvas.clone(), desktop_width, desktop_height).context("canvas initialization")?; + + debug!("Canvas initialized"); + + info!("Start RDP session"); + + let mut image = DecodedImage::new( + PixelFormat::RgbA32, + connection_result.desktop_size.width, + connection_result.desktop_size.height, + ); + + let mut requested_resize = None; + + let mut active_stage = ActiveStage::new(connection_result); + + let disconnect_reason = 'outer: loop { + let outputs = select! { + frame = framed.read_pdu().fuse() => { + let (action, payload) = frame.context("read frame")?; + trace!(?action, frame_length = payload.len(), "Frame received"); + + active_stage.process(&mut image, action, &payload)? + } + input_events = input_events.next() => { + let event = input_events.context("read next input events")?; + + match event { + RdpInputEvent::Cliprdr(message) => { + if let Some(cliprdr) = active_stage.get_svc_processor::() { + if let Some(svc_messages) = match message { + ClipboardMessage::SendInitiateCopy(formats) => Some( + cliprdr.initiate_copy(&formats) + .context("CLIPRDR initiate copy")? + ), + ClipboardMessage::SendFormatData(response) => Some( + cliprdr.submit_format_data(response) + .context("CLIPRDR submit format data")? + ), + ClipboardMessage::SendInitiatePaste(format) => Some( + cliprdr.initiate_paste(format) + .context("CLIPRDR initiate paste")? + ), + ClipboardMessage::Error(e) => { + error!("Clipboard backend error: {}", e); + None + } + } { + let frame = active_stage.process_svc_processor_messages(svc_messages)?; + // Send the messages to the server + vec![ActiveStageOutput::ResponseFrame(frame)] + } else { + // No messages to send to the server + Vec::new() + } + } else { + warn!("Clipboard event received, but Cliprdr is not available"); + Vec::new() + } + } + RdpInputEvent::ClipboardBackend(event) => { + if let Some(clipboard) = &mut clipboard { + clipboard.process_event(event)?; + } + // No RDP output frames for backend event processing + Vec::new() + } + RdpInputEvent::FastPath(events) => { + active_stage.process_fastpath_input(&mut image, &events) + .context("fast path input events processing")? + } + RdpInputEvent::Resize { width, height, scale_factor, physical_size } => { + debug!(width, height, scale_factor, "Resize event received"); + if width == 0 || height == 0 { + warn!("Resize event ignored: width or height is zero"); + Vec::new() + } else if let Some(response_frame) = active_stage.encode_resize(width, height, scale_factor, physical_size) { + let width = NonZeroU32::new(width).expect("width is guaranteed to be non-zero due to the prior check"); + let height = NonZeroU32::new(height).expect("height is guaranteed to be non-zero due to the prior check"); + + requested_resize = Some((width, height)); + vec![ActiveStageOutput::ResponseFrame(response_frame?)] + } else { + debug!("Resize event ignored"); + Vec::new() + } + }, + RdpInputEvent::TerminateSession => { + active_stage.graceful_shutdown() + .context("graceful shutdown")? + } + } + } + }; + + for out in outputs { + match out { + ActiveStageOutput::ResponseFrame(frame) => { + self.writer_tx + .unbounded_send(frame) + .context("Send frame to writer task")?; + } + ActiveStageOutput::GraphicsUpdate(region) => { + // PERF: some copies and conversion could be optimized + let (region, buffer) = extract_partial_image(&image, region); + gui.draw(&buffer, region).context("draw updated region")?; + } + ActiveStageOutput::PointerDefault => { + self.set_cursor_style(CursorStyle::Default)?; + } + ActiveStageOutput::PointerHidden => { + self.set_cursor_style(CursorStyle::Hidden)?; + } + ActiveStageOutput::PointerPosition { .. } => { + // Not applicable for web. + } + ActiveStageOutput::PointerBitmap(pointer) => { + // Maximum allowed cursor size for browsers is 32x32, because bigger sizes + // will cause the following issues: + // - cursors bigger than 128x128 are not supported in browsers. + // - cursors bigger than 32x32 will default to the system cursor if their + // sprite does not fit in the browser's viewport, introducing an abrupt + // cursor style change when the cursor is moved to the edge of the + // browser window. + // + // Therefore, we need to scale the cursor sprite down to 32x32 if it is + // bigger than that. + const MAX_CURSOR_SIZE: u16 = 32; + // INVARIANT: 0 < scale <= 1.0 + // INVARIANT: pointer.width * scale <= MAX_CURSOR_SIZE + // INVARIANT: pointer.height * scale <= MAX_CURSOR_SIZE + let scale = if pointer.width >= pointer.height && pointer.width > MAX_CURSOR_SIZE { + Some(f64::from(MAX_CURSOR_SIZE) / f64::from(pointer.width)) + } else if pointer.height > MAX_CURSOR_SIZE { + Some(f64::from(MAX_CURSOR_SIZE) / f64::from(pointer.height)) + } else { + None + }; + + let (png_width, png_height, hotspot_x, hotspot_y, rgba_buffer) = if let Some(scale) = scale { + // Per invariants: Following conversions will never saturate. + let scaled_width = f64_to_u16_saturating_cast(f64::from(pointer.width) * scale); + let scaled_height = f64_to_u16_saturating_cast(f64::from(pointer.height) * scale); + let hotspot_x = f64_to_u16_saturating_cast(f64::from(pointer.hotspot_x) * scale); + let hotspot_y = f64_to_u16_saturating_cast(f64::from(pointer.hotspot_y) * scale); + + // Per invariants: scaled_width * scaled_height * 4 <= 32 * 32 * 4 < usize::MAX + #[expect(clippy::arithmetic_side_effects)] + let resized_rgba_buffer_size = usize::from(scaled_width * scaled_height * 4); + + let mut rgba_resized = vec![0u8; resized_rgba_buffer_size]; + let mut resizer = resize::new( + usize::from(pointer.width), + usize::from(pointer.height), + usize::from(scaled_width), + usize::from(scaled_height), + resize::Pixel::RGBA8P, + resize::Type::Lanczos3, + ) + .context("failed to initialize cursor resizer")?; + + resizer + .resize(pointer.bitmap_data.as_pixels(), rgba_resized.as_pixels_mut()) + .context("failed to resize cursor")?; + + ( + scaled_width, + scaled_height, + hotspot_x, + hotspot_y, + Cow::Owned(rgba_resized), + ) + } else { + ( + pointer.width, + pointer.height, + pointer.hotspot_x, + pointer.hotspot_y, + Cow::Borrowed(pointer.bitmap_data.as_slice()), + ) + }; + + // Encode PNG. + let mut png_buffer = Vec::new(); + { + let mut encoder = + png::Encoder::new(&mut png_buffer, u32::from(png_width), u32::from(png_height)); + + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + encoder.set_compression(png::Compression::Fast); + let mut writer = encoder.write_header().context("PNG encoder header write failed")?; + writer + .write_image_data(rgba_buffer.as_ref()) + .context("failed to encode pointer PNG")?; + } + + // Encode PNG into Base64 data URL. + let mut style = "data:image/png;base64,".to_owned(); + base64::engine::general_purpose::STANDARD.encode_string(png_buffer, &mut style); + + self.set_cursor_style(CursorStyle::Url { + data: style, + hotspot_x, + hotspot_y, + })?; + } + ActiveStageOutput::DeactivateAll(mut box_connection_activation) => { + // Execute the Deactivation-Reactivation Sequence: + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432 + debug!("Received Server Deactivate All PDU, executing Deactivation-Reactivation Sequence"); + + // We need to perform resize after receiving the Deactivate All PDU, because there may be frames + // with the previous dimensions arriving between the resize request and this message. + if let Some((width, height)) = requested_resize { + self.render_canvas.set_width(width.get()); + self.render_canvas.set_height(height.get()); + gui.resize(width, height); + requested_resize = None; + } + + let mut buf = WriteBuf::new(); + 'activation_seq: loop { + let written = + single_sequence_step_read(&mut framed, &mut *box_connection_activation, &mut buf) + .await?; + + if written.size().is_some() { + self.writer_tx + .unbounded_send(buf.filled().to_vec()) + .context("Send frame to writer task")?; + } + + if let ConnectionActivationState::Finalized { + io_channel_id, + user_channel_id, + desktop_size, + enable_server_pointer, + pointer_software_rendering, + } = box_connection_activation.connection_activation_state() + { + debug!("Deactivation-Reactivation Sequence completed"); + image = DecodedImage::new(PixelFormat::RgbA32, desktop_size.width, desktop_size.height); + // Create a new [`FastPathProcessor`] with potentially updated + // io/user channel ids. + active_stage.set_fastpath_processor( + fast_path::ProcessorBuilder { + io_channel_id, + user_channel_id, + enable_server_pointer, + pointer_software_rendering, + } + .build(), + ); + active_stage.set_enable_server_pointer(enable_server_pointer); + break 'activation_seq; + } + } + } + ActiveStageOutput::Terminate(reason) => break 'outer reason, + } + } + }; + + info!(%disconnect_reason, "RDP session terminated"); + + Ok(SessionTerminationInfo { + reason: disconnect_reason, + }) + } + + fn desktop_size(&self) -> DesktopSize { + DesktopSize { + width: self.desktop_size.width, + height: self.desktop_size.height, + } + } + + fn apply_inputs(&self, transaction: Self::InputTransaction) -> Result<(), Self::Error> { + let inputs = self.input_database.borrow_mut().apply(transaction); + self.h_send_inputs(inputs) + } + + fn release_all_inputs(&self) -> Result<(), Self::Error> { + let inputs = self.input_database.borrow_mut().release_all(); + self.h_send_inputs(inputs) + } + + fn synchronize_lock_keys( + &self, + scroll_lock: bool, + num_lock: bool, + caps_lock: bool, + kana_lock: bool, + ) -> Result<(), Self::Error> { + use ironrdp::pdu::input::fast_path::FastPathInput; + + let event = ironrdp::input::synchronize_event(scroll_lock, num_lock, caps_lock, kana_lock); + let fastpath_input = FastPathInput::single(event); + + let frame = ironrdp::core::encode_vec(&fastpath_input).context("FastPathInput encoding")?; + + self.writer_tx + .unbounded_send(frame) + .context("Send frame to writer task")?; + + Ok(()) + } + + fn shutdown(&self) -> Result<(), Self::Error> { + self.input_events_tx + .unbounded_send(RdpInputEvent::TerminateSession) + .context("failed to send terminate session event to writer task")?; + + Ok(()) + } + + async fn on_clipboard_paste(&self, content: &Self::ClipboardData) -> Result<(), Self::Error> { + self.input_events_tx + .unbounded_send(RdpInputEvent::ClipboardBackend( + WasmClipboardBackendMessage::LocalClipboardChanged(content.clone()), + )) + .context("Send clipboard backend event")?; + + Ok(()) + } + + fn resize( + &self, + width: u32, + height: u32, + scale_factor: Option, + physical_width: Option, + physical_height: Option, + ) { + self.input_events_tx + .unbounded_send(RdpInputEvent::Resize { + width, + height, + scale_factor, + physical_size: physical_width.and_then(|width| physical_height.map(|height| (width, height))), + }) + .expect("send resize event to writer task"); + } + + fn supports_unicode_keyboard_shortcuts(&self) -> bool { + // RDP does not support Unicode keyboard shortcuts. + // When key combinations are executed, only plain scancode events are allowed to function correctly. + false + } + + fn invoke_extension(&self, ext: Extension) -> Result { + Err( + IronError::from(anyhow::Error::msg(format!("unknown extension: {}", ext.ident()))) + .with_kind(IronErrorKind::General), + ) + } +} + +fn build_config( + username: String, + password: String, + domain: Option, + client_name: String, + desktop_size: DesktopSize, +) -> connector::Config { + connector::Config { + credentials: Credentials::UsernamePassword { username, password }, + domain, + // TODO(#327): expose these options from the WASM module. + enable_tls: true, + enable_credssp: true, + keyboard_type: ironrdp::pdu::gcc::KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_layout: 0, // the server SHOULD use the default active input locale identifier + keyboard_functional_keys_count: 12, + ime_file_name: String::new(), + dig_product_id: String::new(), + desktop_size: connector::DesktopSize { + width: desktop_size.width, + height: desktop_size.height, + }, + bitmap: Some(connector::BitmapConfig { + color_depth: 16, + lossy_compression: true, + codecs: client_codecs_capabilities(&[]).expect("can't panic for &[]"), + }), + #[expect( + clippy::arithmetic_side_effects, + reason = "fine unless we end up with an insanely big version" + )] + client_build: semver::Version::parse(env!("CARGO_PKG_VERSION")) + .map_or(0, |version| version.major * 100 + version.minor * 10 + version.patch) + .pipe(u32::try_from) + .expect("fine until major ~42949672"), + client_name, + // NOTE: hardcode this value like in freerdp + // https://github.com/FreeRDP/FreeRDP/blob/4e24b966c86fdf494a782f0dfcfc43a057a2ea60/libfreerdp/core/settings.c#LL49C34-L49C70 + client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), + platform: ironrdp::pdu::rdp::capability_sets::MajorPlatformType::UNSPECIFIED, + enable_server_pointer: false, + autologon: false, + enable_audio_playback: false, + request_data: None, + pointer_software_rendering: false, + performance_flags: PerformanceFlags::default(), + desktop_scale_factor: 0, + hardware_id: None, + license_cache: None, + timezone_info: TimezoneInfo::default(), + } +} + +async fn writer_task( + rx: mpsc::UnboundedReceiver>, + rdp_writer: WriteHalf, + outbound_limit: Option, +) { + debug!("writer task started"); + + async fn inner( + mut rx: mpsc::UnboundedReceiver>, + mut rdp_writer: WriteHalf, + outbound_limit: Option, + ) -> anyhow::Result<()> { + while let Some(frame) = rx.next().await { + match outbound_limit { + Some(max_size) if frame.len() > max_size => { + // Send in chunks. + for chunk in frame.chunks(max_size) { + rdp_writer.write_all(chunk).await.context("couldn't write chunk")?; + rdp_writer.flush().await.context("couldn't flush chunk")?; + } + } + _ => { + // Send complete frame (default case). + rdp_writer.write_all(&frame).await.context("couldn't write frame")?; + rdp_writer.flush().await.context("couldn't flush frame")?; + } + } + } + + Ok(()) + } + + match inner(rx, rdp_writer, outbound_limit).await { + Ok(()) => debug!("writer task ended gracefully"), + Err(e) => error!("writer task ended unexpectedly: {e:#}"), + } +} + +struct ConnectParams { + ws: WebSocket, + config: connector::Config, + proxy_auth_token: String, + destination: String, + pcb: Option, + kdc_proxy_url: Option, + clipboard_backend: Option, + use_display_control: bool, +} + +async fn connect( + ConnectParams { + ws, + config, + proxy_auth_token, + destination, + pcb, + kdc_proxy_url, + clipboard_backend, + use_display_control, + }: ConnectParams, +) -> Result<(connector::ConnectionResult, WebSocket), IronError> { + let mut framed = ironrdp_futures::LocalFuturesFramed::new(ws); + + // In web browser environments, we do not have an easy access to the local address of the socket. + let dummy_client_addr = core::net::SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 33899)); + + let mut connector = ClientConnector::new(config, dummy_client_addr); + + if let Some(clipboard_backend) = clipboard_backend { + connector.attach_static_channel(CliprdrClient::new(Box::new(clipboard_backend))); + } + + if use_display_control { + connector.attach_static_channel( + DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))), + ); + } + + let (upgraded, server_public_key) = + connect_rdcleanpath(&mut framed, &mut connector, destination.clone(), proxy_auth_token, pcb).await?; + + let connection_result = ironrdp_futures::connect_finalize( + upgraded, + connector, + &mut framed, + &mut WasmNetworkClient, + (&destination).into(), + server_public_key, + url::Url::parse(kdc_proxy_url.unwrap_or_default().as_str()) // if kdc_proxy_url does not exit, give url parser a empty string, it will fail anyway and map to a None + .ok() + .map(|url| KerberosConfig { + kdc_proxy_url: Some(url), + // HACK: It's supposed to be the computer name of the client, but since it's not easy to retrieve this information in the browser, + // we set the destination hostname instead because it happens to work. + hostname: Some(destination), + }), + ) + .await?; + + let ws = framed.into_inner_no_leftover(); + + Ok((connection_result, ws)) +} + +async fn connect_rdcleanpath( + framed: &mut ironrdp_futures::Framed, + connector: &mut ClientConnector, + destination: String, + proxy_auth_token: String, + pcb: Option, +) -> Result<(ironrdp_futures::Upgraded, Vec), IronError> +where + S: ironrdp_futures::FramedRead + FramedWrite, +{ + use ironrdp::connector::Sequence as _; + use x509_cert::der::Decode as _; + + #[derive(Clone, Copy, Debug)] + struct RDCleanPathHint; + + const RDCLEANPATH_HINT: RDCleanPathHint = RDCleanPathHint; + + impl ironrdp::pdu::PduHint for RDCleanPathHint { + fn find_size(&self, bytes: &[u8]) -> ironrdp::core::DecodeResult> { + match ironrdp_rdcleanpath::RDCleanPathPdu::detect(bytes) { + ironrdp_rdcleanpath::DetectionResult::Detected { total_length, .. } => Ok(Some((true, total_length))), + ironrdp_rdcleanpath::DetectionResult::NotEnoughBytes => Ok(None), + ironrdp_rdcleanpath::DetectionResult::Failed => Err(ironrdp::core::other_err!( + "RDCleanPathHint", + "detection failed (invalid PDU)" + )), + } + } + } + + let mut buf = WriteBuf::new(); + + info!("Begin connection procedure"); + + { + // RDCleanPath request + + let connector::ClientConnectorState::ConnectionInitiationSendRequest = connector.state else { + return Err(anyhow::Error::msg("invalid connector state (send request)").into()); + }; + + debug_assert!(connector.next_pdu_hint().is_none()); + + let written = connector.step_no_input(&mut buf)?; + let x224_pdu_len = written.size().expect("written size"); + debug_assert_eq!(x224_pdu_len, buf.filled_len()); + let x224_pdu = buf.filled().to_vec(); + + let rdcleanpath_req = + ironrdp_rdcleanpath::RDCleanPathPdu::new_request(x224_pdu, destination, proxy_auth_token, pcb) + .context("new RDCleanPath request")?; + debug!(message = ?rdcleanpath_req, "Send RDCleanPath request"); + let rdcleanpath_req = rdcleanpath_req.to_der().context("RDCleanPath request encode")?; + + framed + .write_all(&rdcleanpath_req) + .await + .context("couldn't write RDCleanPath request")?; + } + + { + // RDCleanPath response + + let rdcleanpath_res = framed + .read_by_hint(&RDCLEANPATH_HINT) + .await + .context("read RDCleanPath request")?; + + let rdcleanpath_res = + ironrdp_rdcleanpath::RDCleanPathPdu::from_der(&rdcleanpath_res).context("RDCleanPath response decode")?; + + debug!(message = ?rdcleanpath_res, "Received RDCleanPath PDU"); + + let (x224_connection_response, server_cert_chain) = + match rdcleanpath_res.into_enum().context("invalid RDCleanPath PDU")? { + ironrdp_rdcleanpath::RDCleanPath::Request { .. } => { + return Err(anyhow::Error::msg("received an unexpected RDCleanPath type (request)").into()); + } + ironrdp_rdcleanpath::RDCleanPath::Response { + x224_connection_response, + server_cert_chain, + server_addr: _, + } => (x224_connection_response, server_cert_chain), + ironrdp_rdcleanpath::RDCleanPath::GeneralErr(error) => { + return Err( + IronError::from(anyhow::Error::new(error).context("received an RDCleanPath error")) + .with_kind(IronErrorKind::RDCleanPath), + ); + } + ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { + x224_connection_response, + } => { + // Try to decode as X.224 Connection Confirm to extract negotiation failure details. + if let Ok(x224_confirm) = ironrdp_core::decode::< + ironrdp::pdu::x224::X224, + >(&x224_connection_response) + { + if let ironrdp::pdu::nego::ConnectionConfirm::Failure { code } = x224_confirm.0 { + // Convert to negotiation failure instead of generic RDCleanPath error. + let negotiation_failure = connector::NegotiationFailure::from(code); + return Err(IronError::from( + anyhow::Error::new(negotiation_failure).context("RDP negotiation failed"), + ) + .with_kind(IronErrorKind::NegotiationFailure)); + } + } + + // Fallback to generic error if we can't decode the negotiation failure. + return Err( + IronError::from(anyhow::Error::msg("received an RDCleanPath negotiation error")) + .with_kind(IronErrorKind::RDCleanPath), + ); + } + }; + + let connector::ClientConnectorState::ConnectionInitiationWaitConfirm { .. } = connector.state else { + return Err(anyhow::Error::msg("invalid connector state (wait confirm)").into()); + }; + + debug_assert!(connector.next_pdu_hint().is_some()); + + buf.clear(); + let written = connector.step(x224_connection_response.as_bytes(), &mut buf)?; + + debug_assert!(written.is_nothing()); + + let server_cert = server_cert_chain + .into_iter() + .next() + .context("server cert chain missing from rdcleanpath response")?; + + let cert = x509_cert::Certificate::from_der(server_cert.as_bytes()) + .context("failed to decode x509 certificate sent by proxy")?; + + let server_public_key = cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() + .context("subject public key BIT STRING is not aligned")? + .to_owned(); + + let should_upgrade = ironrdp_futures::skip_connect_begin(connector); + + // At this point, proxy established the TLS session. + + let upgraded = ironrdp_futures::mark_as_upgraded(should_upgrade, connector); + + Ok((upgraded, server_public_key)) + } +} + +#[expect(clippy::as_conversions, clippy::cast_sign_loss, clippy::cast_possible_truncation)] +fn f64_to_u16_saturating_cast(value: f64) -> u16 { + value as u16 +} diff --git a/crates/ironrdp-web/src/utils.rs b/crates/ironrdp-web/src/utils.rs new file mode 100644 index 00000000..b1d7929d --- /dev/null +++ b/crates/ironrdp-web/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/crates/ironrdp/CHANGELOG.md b/crates/ironrdp/CHANGELOG.md new file mode 100644 index 00000000..48a74967 --- /dev/null +++ b/crates/ironrdp/CHANGELOG.md @@ -0,0 +1,91 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.14.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.13.0...ironrdp-v0.14.0)] - 2025-12-18 + +### Build + +- Bump picky and sspi ([#1028](https://github.com/Devolutions/IronRDP/issues/1028)) ([5bd319126d](https://github.com/Devolutions/IronRDP/commit/5bd319126d32fbd8e505508e27ab2b1a18a83d04)) + + This fixes build issues with some dependencies. + +## [[0.13.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.12.0...ironrdp-v0.13.0)] - 2025-09-24 + +### Build + +- Update dependencies + +## [[0.12.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.11.0...ironrdp-v0.12.0)] - 2025-08-29 + +### Build + +- Update dependencies + +## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.10.0...ironrdp-v0.11.0)] - 2025-07-08 + +### Build + +- Update dependencies + +## [[0.9.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.9.0...ironrdp-v0.9.1)] - 2025-03-13 + +### Documentation + +- Fix documentation build (#700) ([0705840aa5](https://github.com/Devolutions/IronRDP/commit/0705840aa51bc920e76f0cf1fce06b29733c6e2d)) + +## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.8.0...ironrdp-v0.9.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.7.4...ironrdp-v0.8.0)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + +## [[0.7.4](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.7.3...ironrdp-v0.7.4)] - 2025-01-28 + +### Build + +- Update dependencies + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + +- Extend server example to demonstrate Opus audio codec support (#643) ([fa353765af](https://github.com/Devolutions/IronRDP/commit/fa353765af016734c07e31fff44d19dabfdd4199)) + + +## [[0.7.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.7.2...ironrdp-v0.7.3)] - 2024-12-16 + +### Documentation + +- Inline documentation for re-exported items (#619) ([cff5c1a59c](https://github.com/Devolutions/IronRDP/commit/cff5c1a59cdc2da73cabcb675fcf2d85dc81fd68)) + + + +## [[0.7.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.7.1...ironrdp-v0.7.2)] - 2024-12-15 + +### Documentation + +- Fix server example ([#616](https://github.com/Devolutions/IronRDP/pull/616)) ([02c6fd5dfe](https://github.com/Devolutions/IronRDP/commit/02c6fd5dfe142b7cc6f15cb17292504657818498)) + + The rt-multi-thread feature of tokio is not enabled when compiling the + example alone (without feature unification from other crates of the + workspace). + + + +## [[0.7.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.7.0...ironrdp-v0.7.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) + diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml new file mode 100644 index 00000000..25945735 --- /dev/null +++ b/crates/ironrdp/Cargo.toml @@ -0,0 +1,86 @@ +[package] +name = "ironrdp" +version = "0.14.0" +readme = "README.md" +description = "A meta crate re-exporting IronRDP crates for convenience" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = ["core", "pdu"] +core = ["dep:ironrdp-core"] +pdu = ["dep:ironrdp-pdu"] +cliprdr = ["dep:ironrdp-cliprdr"] +connector = ["dep:ironrdp-connector"] +acceptor = ["dep:ironrdp-acceptor"] +session = ["dep:ironrdp-session"] +graphics = ["dep:ironrdp-graphics"] +input = ["dep:ironrdp-input"] +server = ["dep:ironrdp-server"] +svc = ["dep:ironrdp-svc"] +dvc = ["dep:ironrdp-dvc"] +rdpdr = ["dep:ironrdp-rdpdr"] +rdpsnd = ["dep:ironrdp-rdpsnd"] +displaycontrol = ["dep:ironrdp-displaycontrol"] +qoi = ["ironrdp-server?/qoi", "ironrdp-pdu?/qoi", "ironrdp-connector?/qoi", "ironrdp-session?/qoi"] +qoiz = ["ironrdp-server?/qoiz", "ironrdp-pdu?/qoiz", "ironrdp-connector?/qoiz", "ironrdp-session?/qoiz"] +# Internal (PRIVATE!) features used to aid testing. +# Don't rely on these whatsoever. They may disappear at any time. +__bench = ["ironrdp-server/__bench"] + +[dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1", optional = true } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6", optional = true } # public +ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.5", optional = true } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.8", optional = true } # public +ironrdp-acceptor = { path = "../ironrdp-acceptor", version = "0.8", optional = true } # public +ironrdp-session = { path = "../ironrdp-session", version = "0.8", optional = true } # public +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.7", optional = true } # public +ironrdp-input = { path = "../ironrdp-input", version = "0.4", optional = true } # public +ironrdp-server = { path = "../ironrdp-server", version = "0.10", optional = true, features = ["helper"] } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.5", optional = true } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.4", optional = true } # public +ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.5", optional = true } # public +ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.6", optional = true } # public +ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.4", optional = true } # public + +[dev-dependencies] +ironrdp-blocking = { path = "../ironrdp-blocking", version = "0.8.0" } +ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.5.0" } +anyhow = "1" +async-trait = "0.1" +image = { version = "0.25.6", default-features = false, features = ["png"] } +pico-args = "0.5" +x509-cert = { version = "0.2", default-features = false, features = ["std"] } +sspi = { version = "0.18", features = ["network_client"] } +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tokio-rustls = "0.26" +rand = "0.9" +opus2 = "0.3" + +[package.metadata.docs.rs] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] +all-features = true + +[[example]] +name = "screenshot" +doc-scrape-examples = true +required-features = ["session", "connector", "graphics"] + +[[example]] +name = "server" +doc-scrape-examples = true +required-features = ["cliprdr", "connector", "rdpsnd", "server"] + +[lints] +workspace = true diff --git a/crates/ironrdp/LICENSE-APACHE b/crates/ironrdp/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/ironrdp/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ironrdp/LICENSE-MIT b/crates/ironrdp/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/ironrdp/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/ironrdp/README.md b/crates/ironrdp/README.md new file mode 100644 index 00000000..651395d8 --- /dev/null +++ b/crates/ironrdp/README.md @@ -0,0 +1,7 @@ +# IronRDP meta crate + +A meta crate re-exporting IronRDP crates for convenience. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/crates/ironrdp/examples/screenshot.rs b/crates/ironrdp/examples/screenshot.rs new file mode 100644 index 00000000..64ff33ac --- /dev/null +++ b/crates/ironrdp/examples/screenshot.rs @@ -0,0 +1,427 @@ +//! Example of utilizing IronRDP in a blocking, synchronous fashion. +//! +//! This example showcases the use of IronRDP in a blocking manner. It +//! demonstrates how to create a basic RDP client with just a few hundred lines +//! of code by leveraging the IronRDP crates suite. +//! +//! In this basic client implementation, the client establishes a connection +//! with the destination server, decodes incoming graphics updates, and saves the +//! resulting output as a PNG image file on the disk. +//! +//! # Usage example +//! +//! ```shell +//! cargo run --example=screenshot -- --host -u -p -o out.png +//! ``` + +#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary +#![allow(clippy::print_stdout)] + +use core::time::Duration; +use std::io::Write as _; +use std::net::TcpStream; +use std::path::PathBuf; + +use anyhow::Context as _; +use connector::Credentials; +use ironrdp::connector; +use ironrdp::connector::ConnectionResult; +use ironrdp::pdu::gcc::KeyboardType; +use ironrdp::pdu::rdp::capability_sets::MajorPlatformType; +use ironrdp::session::image::DecodedImage; +use ironrdp::session::{ActiveStage, ActiveStageOutput}; +use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; +use sspi::network_client::reqwest_network_client::ReqwestNetworkClient; +use tokio_rustls::rustls; +use tracing::{debug, info, trace}; + +const HELP: &str = "\ +USAGE: + cargo run --example=screenshot -- --host --port + -u/--username -p/--password + [-o/--output ] [-d/--domain ] +"; + +fn main() -> anyhow::Result<()> { + let action = match parse_args() { + Ok(action) => action, + Err(e) => { + println!("{HELP}"); + return Err(e.context("invalid argument(s)")); + } + }; + + setup_logging()?; + + match action { + Action::ShowHelp => { + println!("{HELP}"); + Ok(()) + } + Action::Run { + host, + port, + username, + password, + output, + domain, + } => { + info!(host, port, username, password, output = %output.display(), domain, "run"); + run(host, port, username, password, output, domain) + } + } +} + +#[derive(Debug)] +enum Action { + ShowHelp, + Run { + host: String, + port: u16, + username: String, + password: String, + output: PathBuf, + domain: Option, + }, +} + +fn parse_args() -> anyhow::Result { + let mut args = pico_args::Arguments::from_env(); + + let action = if args.contains(["-h", "--help"]) { + Action::ShowHelp + } else { + let host = args.value_from_str("--host")?; + let port = args.opt_value_from_str("--port")?.unwrap_or(3389); + let username = args.value_from_str(["-u", "--username"])?; + let password = args.value_from_str(["-p", "--password"])?; + let output = args + .opt_value_from_str(["-o", "--output"])? + .unwrap_or_else(|| PathBuf::from("out.png")); + let domain = args.opt_value_from_str(["-d", "--domain"])?; + + Action::Run { + host, + port, + username, + password, + output, + domain, + } + }; + + Ok(action) +} + +fn setup_logging() -> anyhow::Result<()> { + use tracing::metadata::LevelFilter; + use tracing_subscriber::prelude::*; + use tracing_subscriber::EnvFilter; + + let fmt_layer = tracing_subscriber::fmt::layer().compact(); + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .with_env_var("IRONRDP_LOG") + .from_env_lossy(); + + tracing_subscriber::registry() + .with(fmt_layer) + .with(env_filter) + .try_init() + .context("failed to set tracing global subscriber")?; + + Ok(()) +} + +fn run( + server_name: String, + port: u16, + username: String, + password: String, + output: PathBuf, + domain: Option, +) -> anyhow::Result<()> { + let config = build_config(username, password, domain); + + let (connection_result, framed) = connect(config, server_name, port).context("connect")?; + + let mut image = DecodedImage::new( + ironrdp_graphics::image_processing::PixelFormat::RgbA32, + connection_result.desktop_size.width, + connection_result.desktop_size.height, + ); + + active_stage(connection_result, framed, &mut image).context("active stage")?; + + let img: image::ImageBuffer, _> = + image::ImageBuffer::from_raw(u32::from(image.width()), u32::from(image.height()), image.data()) + .context("invalid image")?; + + img.save(output).context("save image to disk")?; + + Ok(()) +} + +fn build_config(username: String, password: String, domain: Option) -> connector::Config { + connector::Config { + credentials: Credentials::UsernamePassword { username, password }, + domain, + enable_tls: false, // This example does not expose any frontend. + enable_credssp: true, + keyboard_type: KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_layout: 0, + keyboard_functional_keys_count: 12, + ime_file_name: String::new(), + dig_product_id: String::new(), + desktop_size: connector::DesktopSize { + width: 1280, + height: 1024, + }, + bitmap: None, + client_build: 0, + client_name: "ironrdp-screenshot-example".to_owned(), + client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), + + #[cfg(windows)] + platform: MajorPlatformType::WINDOWS, + #[cfg(target_os = "macos")] + platform: MajorPlatformType::MACINTOSH, + #[cfg(target_os = "ios")] + platform: MajorPlatformType::IOS, + #[cfg(target_os = "linux")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "android")] + platform: MajorPlatformType::ANDROID, + #[cfg(target_os = "freebsd")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "dragonfly")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "openbsd")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "netbsd")] + platform: MajorPlatformType::UNIX, + + enable_server_pointer: false, // Disable custom pointers (there is no user interaction anyway). + request_data: None, + autologon: false, + enable_audio_playback: false, + pointer_software_rendering: true, + performance_flags: PerformanceFlags::default(), + desktop_scale_factor: 0, + hardware_id: None, + license_cache: None, + timezone_info: TimezoneInfo::default(), + } +} + +type UpgradedFramed = ironrdp_blocking::Framed>; + +fn connect( + config: connector::Config, + server_name: String, + port: u16, +) -> anyhow::Result<(ConnectionResult, UpgradedFramed)> { + let server_addr = lookup_addr(&server_name, port).context("lookup addr")?; + + info!(%server_addr, "Looked up server address"); + + let tcp_stream = TcpStream::connect(server_addr).context("TCP connect")?; + + // Sets the read timeout for the TCP stream so we can break out of the + // infinite loop during the active stage once there is no more activity. + tcp_stream + .set_read_timeout(Some(Duration::from_secs(3))) + .expect("set_read_timeout call failed"); + + let client_addr = tcp_stream.local_addr().context("get socket local address")?; + + let mut framed = ironrdp_blocking::Framed::new(tcp_stream); + + let mut connector = connector::ClientConnector::new(config, client_addr); + + let should_upgrade = ironrdp_blocking::connect_begin(&mut framed, &mut connector).context("begin connection")?; + + debug!("TLS upgrade"); + + // Ensure there is no leftover + let initial_stream = framed.into_inner_no_leftover(); + + let (upgraded_stream, server_public_key) = + tls_upgrade(initial_stream, server_name.clone()).context("TLS upgrade")?; + + let upgraded = ironrdp_blocking::mark_as_upgraded(should_upgrade, &mut connector); + + let mut upgraded_framed = ironrdp_blocking::Framed::new(upgraded_stream); + + let mut network_client = ReqwestNetworkClient; + let connection_result = ironrdp_blocking::connect_finalize( + upgraded, + connector, + &mut upgraded_framed, + &mut network_client, + server_name.into(), + server_public_key, + None, + ) + .context("finalize connection")?; + + Ok((connection_result, upgraded_framed)) +} + +fn active_stage( + connection_result: ConnectionResult, + mut framed: UpgradedFramed, + image: &mut DecodedImage, +) -> anyhow::Result<()> { + let mut active_stage = ActiveStage::new(connection_result); + + 'outer: loop { + let (action, payload) = match framed.read_pdu() { + Ok((action, payload)) => (action, payload), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break 'outer, + Err(e) => return Err(anyhow::Error::new(e).context("read frame")), + }; + + trace!(?action, frame_length = payload.len(), "Frame received"); + + let outputs = active_stage.process(image, action, &payload)?; + + for out in outputs { + match out { + ActiveStageOutput::ResponseFrame(frame) => framed.write_all(&frame).context("write response")?, + ActiveStageOutput::Terminate(_) => break 'outer, + _ => {} + } + } + } + + Ok(()) +} + +fn lookup_addr(hostname: &str, port: u16) -> anyhow::Result { + use std::net::ToSocketAddrs as _; + let addr = (hostname, port) + .to_socket_addrs()? + .next() + .context("socket address not found")?; + Ok(addr) +} + +fn tls_upgrade( + stream: TcpStream, + server_name: String, +) -> anyhow::Result<(rustls::StreamOwned, Vec)> { + let mut config = rustls::client::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(std::sync::Arc::new(danger::NoCertificateVerification)) + .with_no_client_auth(); + + // This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret) + config.key_log = std::sync::Arc::new(rustls::KeyLogFile::new()); + + // Disable TLS resumption because it’s not supported by some services such as CredSSP. + // + // > The CredSSP Protocol does not extend the TLS wire protocol. TLS session resumption is not supported. + // + // source: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/385a7489-d46b-464c-b224-f7340e308a5c + config.resumption = rustls::client::Resumption::disabled(); + + let config = std::sync::Arc::new(config); + + let server_name = server_name.try_into()?; + + let client = rustls::ClientConnection::new(config, server_name)?; + + let mut tls_stream = rustls::StreamOwned::new(client, stream); + + // We need to flush in order to ensure the TLS handshake is moving forward. Without flushing, + // it’s likely the peer certificate is not yet received a this point. + tls_stream.flush()?; + + let cert = tls_stream + .conn + .peer_certificates() + .and_then(|certificates| certificates.first()) + .context("peer certificate is missing")?; + + let server_public_key = extract_tls_server_public_key(cert)?; + + Ok((tls_stream, server_public_key)) +} + +fn extract_tls_server_public_key(cert: &[u8]) -> anyhow::Result> { + use x509_cert::der::Decode as _; + + let cert = x509_cert::Certificate::from_der(cert)?; + + debug!(%cert.tbs_certificate.subject); + + let server_public_key = cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() + .context("subject public key BIT STRING is not aligned")? + .to_owned(); + + Ok(server_public_key) +} + +mod danger { + use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; + use tokio_rustls::rustls::{pki_types, DigitallySignedStruct, Error, SignatureScheme}; + + #[derive(Debug)] + pub(super) struct NoCertificateVerification; + + impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _: &pki_types::CertificateDer<'_>, + _: &[pki_types::CertificateDer<'_>], + _: &pki_types::ServerName<'_>, + _: &[u8], + _: pki_types::UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _: &[u8], + _: &pki_types::CertificateDer<'_>, + _: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _: &[u8], + _: &pki_types::CertificateDer<'_>, + _: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } + } +} diff --git a/crates/ironrdp/examples/server.rs b/crates/ironrdp/examples/server.rs new file mode 100644 index 00000000..94e4a9ac --- /dev/null +++ b/crates/ironrdp/examples/server.rs @@ -0,0 +1,441 @@ +//! Example of utilizing `ironrdp-server` crate. + +#![allow(unused_crate_dependencies)] // False positives because there are both a library and a binary. +#![allow(clippy::print_stdout)] + +use core::net::SocketAddr; +use core::num::{NonZeroU16, NonZeroUsize}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +use anyhow::Context as _; +use ironrdp::cliprdr::backend::{CliprdrBackend, CliprdrBackendFactory}; +use ironrdp::connector::DesktopSize; +use ironrdp::rdpsnd::pdu::{AudioFormat, ClientAudioFormatPdu, WaveFormat}; +use ironrdp::rdpsnd::server::{RdpsndServerHandler, RdpsndServerMessage}; +use ironrdp::server::tokio::sync::mpsc::UnboundedSender; +use ironrdp::server::tokio::time::{self, sleep, Duration}; +use ironrdp::server::{ + tokio, BitmapUpdate, CliprdrServerFactory, Credentials, DisplayUpdate, KeyboardEvent, MouseEvent, PixelFormat, + RdpServer, RdpServerDisplay, RdpServerDisplayUpdates, RdpServerInputHandler, ServerEvent, ServerEventSender, + SoundServerFactory, TlsIdentityCtx, +}; +use ironrdp_cliprdr_native::StubCliprdrBackend; +use rand::prelude::*; +use tracing::{debug, info, warn}; + +const HELP: &str = "\ +USAGE: + cargo run --example=server -- [--bind-addr ] [--cert ] [--key ] [--user USERNAME] [--pass PASSWORD] [--sec tls|hybrid] +"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), anyhow::Error> { + let action = match parse_args() { + Ok(action) => action, + Err(e) => { + println!("{HELP}"); + return Err(e.context("invalid argument(s)")); + } + }; + + setup_logging()?; + + match action { + Action::ShowHelp => { + println!("{HELP}"); + Ok(()) + } + Action::Run { + bind_addr, + hybrid, + user, + pass, + cert, + key, + } => run(bind_addr, hybrid, user, pass, cert, key).await, + } +} + +#[derive(Debug)] +enum Action { + ShowHelp, + Run { + bind_addr: SocketAddr, + hybrid: bool, + user: String, + pass: String, + cert: Option, + key: Option, + }, +} + +fn parse_args() -> anyhow::Result { + let mut args = pico_args::Arguments::from_env(); + + let action = if args.contains(["-h", "--help"]) { + Action::ShowHelp + } else { + let bind_addr = args + .opt_value_from_str("--bind-addr")? + .unwrap_or_else(|| "127.0.0.1:3389".parse().expect("valid hardcoded SocketAddr string")); + + let sec = args.opt_value_from_str("--sec")?.unwrap_or_else(|| "hybrid".to_owned()); + let hybrid = match sec.as_ref() { + "tls" => false, + "hybrid" => true, + _ => anyhow::bail!("Unhandled security: '{sec}'"), + }; + + let cert = args.opt_value_from_str("--cert")?; + let key = args.opt_value_from_str("--key")?; + + let user = args.opt_value_from_str("--user")?.unwrap_or_else(|| "user".to_owned()); + let pass = args.opt_value_from_str("--pass")?.unwrap_or_else(|| "pass".to_owned()); + + Action::Run { + bind_addr, + hybrid, + user, + pass, + cert, + key, + } + }; + + Ok(action) +} + +fn setup_logging() -> anyhow::Result<()> { + use tracing::metadata::LevelFilter; + use tracing_subscriber::prelude::*; + use tracing_subscriber::EnvFilter; + + let fmt_layer = tracing_subscriber::fmt::layer().compact(); + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .with_env_var("IRONRDP_LOG") + .from_env_lossy(); + + tracing_subscriber::registry() + .with(fmt_layer) + .with(env_filter) + .try_init() + .context("failed to set tracing global subscriber")?; + + Ok(()) +} + +#[derive(Clone, Debug)] +struct Handler; + +impl Handler { + fn new() -> Self { + Self + } +} + +impl RdpServerInputHandler for Handler { + fn keyboard(&mut self, event: KeyboardEvent) { + info!(?event, "keyboard"); + } + + fn mouse(&mut self, event: MouseEvent) { + info!(?event, "mouse"); + } +} + +const WIDTH: u16 = 1920; +const HEIGHT: u16 = 1080; + +struct DisplayUpdates; + +#[async_trait::async_trait] +impl RdpServerDisplayUpdates for DisplayUpdates { + async fn next_update(&mut self) -> anyhow::Result> { + sleep(Duration::from_millis(100)).await; + let mut rng = rand::rng(); + + let y: u16 = rng.random_range(0..HEIGHT); + let height = rng.random_range(1..=HEIGHT.checked_sub(y).expect("never underflow")); + let height = NonZeroU16::new(height).expect("never zero"); + + let x: u16 = rng.random_range(0..WIDTH); + let width = rng.random_range(1..=WIDTH.checked_sub(x).expect("never underflow")); + let width = NonZeroU16::new(width).expect("never zero"); + + let capacity = NonZeroUsize::from(width) + .checked_mul(NonZeroUsize::from(height)) + .expect("never overflow") + .get() + .checked_mul(4) + .expect("never overflow"); + let mut data = Vec::with_capacity(capacity); + for _ in 0..(data.capacity() / 4) { + data.push(rng.random()); + data.push(rng.random()); + data.push(rng.random()); + data.push(255); + } + + info!("get_update +{x}+{y} {width}x{height}"); + let stride = NonZeroUsize::from(width) + .checked_mul(NonZeroUsize::new(4).expect("never zero")) + .expect("never overflow"); + let bitmap = BitmapUpdate { + x, + y, + width, + height, + format: PixelFormat::BgrA32, + data: data.into(), + stride, + }; + Ok(Some(DisplayUpdate::Bitmap(bitmap))) + } +} + +#[async_trait::async_trait] +impl RdpServerDisplay for Handler { + async fn size(&mut self) -> DesktopSize { + DesktopSize { + width: WIDTH, + height: HEIGHT, + } + } + + async fn updates(&mut self) -> anyhow::Result> { + Ok(Box::new(DisplayUpdates {})) + } +} + +struct StubCliprdrServerFactory; + +impl CliprdrBackendFactory for StubCliprdrServerFactory { + fn build_cliprdr_backend(&self) -> Box { + Box::new(StubCliprdrBackend::new()) + } +} + +impl ServerEventSender for StubCliprdrServerFactory { + fn set_sender(&mut self, _sender: UnboundedSender) {} +} + +impl CliprdrServerFactory for StubCliprdrServerFactory {} + +#[derive(Debug)] +pub struct Inner { + ev_sender: Option>, +} + +struct StubSoundServerFactory { + inner: Arc>, +} + +impl ServerEventSender for StubSoundServerFactory { + fn set_sender(&mut self, sender: UnboundedSender) { + let mut inner = self.inner.lock().expect("poisoned"); + inner.ev_sender = Some(sender); + } +} + +impl SoundServerFactory for StubSoundServerFactory { + fn build_backend(&self) -> Box { + Box::new(SndHandler { + inner: Arc::clone(&self.inner), + task: None, + }) + } +} + +#[derive(Debug)] +struct SndHandler { + inner: Arc>, + task: Option>, +} + +impl SndHandler { + fn choose_format(&self, client_formats: &[AudioFormat]) -> Option { + for (n, fmt) in client_formats.iter().enumerate() { + if self.get_formats().contains(fmt) { + return u16::try_from(n).ok(); + } + } + None + } +} + +impl RdpsndServerHandler for SndHandler { + fn get_formats(&self) -> &[AudioFormat] { + &[ + AudioFormat { + format: WaveFormat::OPUS, + n_channels: 2, + n_samples_per_sec: 48000, + n_avg_bytes_per_sec: 192000, + n_block_align: 4, + bits_per_sample: 16, + data: None, + }, + AudioFormat { + format: WaveFormat::PCM, + n_channels: 2, + n_samples_per_sec: 44100, + n_avg_bytes_per_sec: 176400, + n_block_align: 4, + bits_per_sample: 16, + data: None, + }, + ] + } + + fn start(&mut self, client_format: &ClientAudioFormatPdu) -> Option { + debug!(?client_format); + + let Some(nfmt) = self.choose_format(&client_format.formats) else { + return Some(0); + }; + + let fmt = client_format.formats[usize::from(nfmt)].clone(); + + let mut opus_enc = if fmt.format == WaveFormat::OPUS { + let n_channels: opus2::Channels = match fmt.n_channels { + 1 => opus2::Channels::Mono, + 2 => opus2::Channels::Stereo, + n => { + warn!("Invalid OPUS channels: {}", n); + return Some(0); + } + }; + + match opus2::Encoder::new(fmt.n_samples_per_sec, n_channels, opus2::Application::Audio) { + Ok(enc) => Some(enc), + Err(err) => { + warn!("Failed to create OPUS encoder: {}", err); + return Some(0); + } + } + } else { + None + }; + + let inner = Arc::clone(&self.inner); + self.task = Some(tokio::spawn(async move { + let mut interval = time::interval(Duration::from_millis(20)); + let mut ts = 0; + let mut phase = 0.0f32; + loop { + interval.tick().await; + let wave = generate_sine_wave(fmt.n_samples_per_sec, 440.0, 20, &mut phase); + + let data = if let Some(ref mut enc) = opus_enc { + match enc.encode_vec(&wave, wave.len()) { + Ok(data) => data, + Err(err) => { + warn!("Failed to encode with OPUS: {}", err); + return; + } + } + } else { + wave.into_iter().flat_map(|value| value.to_le_bytes()).collect() + }; + + let inner = inner.lock().expect("poisoned"); + if let Some(sender) = inner.ev_sender.as_ref() { + let _ = sender.send(ServerEvent::Rdpsnd(RdpsndServerMessage::Wave(data, ts))); + } + ts = ts.wrapping_add(100); + } + })); + + Some(nfmt) + } + + fn stop(&mut self) { + let Some(task) = self.task.take() else { + return; + }; + task.abort(); + } +} + +fn generate_sine_wave(sample_rate: u32, frequency: f32, duration_ms: u64, phase: &mut f32) -> Vec { + use core::f32::consts::PI; + + let total_samples = (u64::from(sample_rate) * duration_ms) / 1000; + + #[expect(clippy::as_conversions)] + let delta_phase = 2.0 * PI * frequency / sample_rate as f32; + + let amplitude = 32767.0; // Max amplitude for 16-bit audio + + let capacity = usize::try_from(total_samples).expect("u64-to-usize") * 2; // 2 channels + let mut samples = Vec::with_capacity(capacity); + + for _ in 0..total_samples { + let sample = (*phase).sin(); + *phase += delta_phase; + + // Wrap phase to maintain precision and avoid overflow. + *phase %= 2.0 * PI; + + #[expect(clippy::as_conversions, clippy::cast_possible_truncation)] + let sample_i16 = (sample * amplitude) as i16; + + // Write same sample to both channels (stereo) + samples.push(sample_i16); + samples.push(sample_i16); + } + + samples +} + +async fn run( + bind_addr: SocketAddr, + hybrid: bool, + username: String, + password: String, + cert: Option, + key: Option, +) -> anyhow::Result<()> { + info!(%bind_addr, ?cert, ?key, "run"); + + let handler = Handler::new(); + + let server_builder = RdpServer::builder().with_addr(bind_addr); + + let server_builder = if let Some((cert_path, key_path)) = cert.as_deref().zip(key.as_deref()) { + let identity = TlsIdentityCtx::init_from_paths(cert_path, key_path).context("failed to init TLS identity")?; + let acceptor = identity.make_acceptor().context("failed to build TLS acceptor")?; + + if hybrid { + server_builder.with_hybrid(acceptor, identity.pub_key) + } else { + server_builder.with_tls(acceptor) + } + } else { + server_builder.with_no_security() + }; + + let cliprdr = Box::new(StubCliprdrServerFactory); + + let sound = Box::new(StubSoundServerFactory { + inner: Arc::new(Mutex::new(Inner { ev_sender: None })), + }); + + let mut server = server_builder + .with_input_handler(handler.clone()) + .with_display_handler(handler.clone()) + .with_cliprdr_factory(Some(cliprdr)) + .with_sound_factory(Some(sound)) + .build(); + + server.set_credentials(Some(Credentials { + username, + password, + domain: None, + })); + + server.run().await +} diff --git a/crates/ironrdp/src/lib.rs b/crates/ironrdp/src/lib.rs new file mode 100644 index 00000000..4c34571c --- /dev/null +++ b/crates/ironrdp/src/lib.rs @@ -0,0 +1,65 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![cfg_attr(rustfmt, rustfmt_skip)] + +#[cfg(test)] +use { + anyhow as _, async_trait as _, image as _, ironrdp_blocking as _, ironrdp_cliprdr_native as _, opus2 as _, + pico_args as _, rand as _, sspi as _, tokio_rustls as _, tracing as _, tracing_subscriber as _, x509_cert as _, +}; + +#[cfg(feature = "acceptor")] +#[doc(inline)] +pub use ironrdp_acceptor as acceptor; + +#[cfg(feature = "cliprdr")] +#[doc(inline)] +pub use ironrdp_cliprdr as cliprdr; + +#[cfg(feature = "connector")] +#[doc(inline)] +pub use ironrdp_connector as connector; + +#[cfg(feature = "core")] +#[doc(inline)] +pub use ironrdp_core as core; + +#[cfg(feature = "displaycontrol")] +#[doc(inline)] +pub use ironrdp_displaycontrol as displaycontrol; + +#[cfg(feature = "dvc")] +#[doc(inline)] +pub use ironrdp_dvc as dvc; + +#[cfg(feature = "graphics")] +#[doc(inline)] +pub use ironrdp_graphics as graphics; + +#[cfg(feature = "input")] +#[doc(inline)] +pub use ironrdp_input as input; + +#[cfg(feature = "pdu")] +#[doc(inline)] +pub use ironrdp_pdu as pdu; + +#[cfg(feature = "rdpdr")] +#[doc(inline)] +pub use ironrdp_rdpdr as rdpdr; + +#[cfg(feature = "rdpsnd")] +#[doc(inline)] +pub use ironrdp_rdpsnd as rdpsnd; + +#[cfg(feature = "server")] +#[doc(inline)] +pub use ironrdp_server as server; + +#[cfg(feature = "session")] +#[doc(inline)] +pub use ironrdp_session as session; + +#[cfg(feature = "svc")] +#[doc(inline)] +pub use ironrdp_svc as svc; diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml new file mode 100644 index 00000000..574b77d3 --- /dev/null +++ b/ffi/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "ffi" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +name = "ironrdp" +crate-type = ["staticlib", "cdylib"] +doc = false +test = false +doctest = false + +[dependencies] +diplomat = "0.7" +diplomat-runtime = "0.7" +ironrdp = { path = "../crates/ironrdp", features = ["session", "connector", "dvc", "svc", "rdpdr", "rdpsnd", "graphics", "input", "cliprdr", "displaycontrol"] } +ironrdp-cliprdr-native.path = "../crates/ironrdp-cliprdr-native" +ironrdp-dvc-pipe-proxy.path = "../crates/ironrdp-dvc-pipe-proxy" +ironrdp-core = { path = "../crates/ironrdp-core", features = ["alloc"] } +ironrdp-rdcleanpath.path = "../crates/ironrdp-rdcleanpath" +sspi = { version = "0.18", features = ["network_client"] } +thiserror = "2" +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1.0" + +[target.'cfg(windows)'.build-dependencies] +embed-resource = "3.0" + +[lints] +workspace = true diff --git a/ffi/README.md b/ffi/README.md new file mode 100644 index 00000000..9e7cecf1 --- /dev/null +++ b/ffi/README.md @@ -0,0 +1,21 @@ +# IronRDP FFI + +[Diplomat]-based FFI for IronRDP. + +Currently, only the .NET target is officially supported. + +## How to build + +- Install required tools: `cargo xtask ffi install` + - For .NET, note that `dotnet` is also a requirement that you will need to install on your own. + +- Build the shared library: `cargo xtask ffi build` (alternatively, in release mode: `cargo xtask ffi build --release`) + +- Build the bindings: `cargo xtask ffi bindings` + +At this point, you may build and run the examples for .NET: + +- `dotnet run --project Devolutions.IronRdp.ConnectExample` +- `dotnet run --project Devolutions.IronRdp.AvaloniaExample` + +[Diplomat]: https://github.com/rust-diplomat/diplomat diff --git a/ffi/build.rs b/ffi/build.rs new file mode 100644 index 00000000..98bb2567 --- /dev/null +++ b/ffi/build.rs @@ -0,0 +1,93 @@ +#[cfg(not(target_os = "windows"))] +use other::main_stub; +#[cfg(target_os = "windows")] +use win::main_stub; + +fn main() { + main_stub() +} + +#[cfg(target_os = "windows")] +mod win { + use std::env; + use std::fs::File; + use std::io::Write as _; + + fn generate_version_rc() -> String { + let output_name = "DevolutionsIronRdp"; + let filename = format!("{output_name}.dll"); + let company_name = "Devolutions Inc."; + let legal_copyright = format!("Copyright 2019-2024 {company_name}"); + + let mut cargo_version = + env::var("CARGO_PKG_VERSION").expect("failed to fetch `CARGO_PKG_VERSION` environment variable"); + cargo_version.push_str(".0"); + + let version_number = cargo_version; + let version_commas = version_number.replace('.', ","); + let file_description = output_name; + let file_version = version_number.clone(); + let internal_name = filename.clone(); + let original_filename = filename; + let product_name = output_name; + let product_version = version_number; + let vs_file_version = version_commas.clone(); + let vs_product_version = version_commas; + + let version_rc = format!( + r#"#include +VS_VERSION_INFO VERSIONINFO + FILEVERSION {vs_file_version} + PRODUCTVERSION {vs_product_version} + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "{company_name}" + VALUE "FileDescription", "{file_description}" + VALUE "FileVersion", "{file_version}" + VALUE "InternalName", "{internal_name}" + VALUE "LegalCopyright", "{legal_copyright}" + VALUE "OriginalFilename", "{original_filename}" + VALUE "ProductName", "{product_name}" + VALUE "ProductVersion", "{product_version}" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END +"# + ); + + version_rc + } + + pub(crate) fn main_stub() { + let out_dir = env::var("OUT_DIR").expect("failed to fetch `OUT_DIR` environment variable"); + let version_rc_file = format!("{out_dir}/version.rc"); + let version_rc_data = generate_version_rc(); + let mut file = File::create(&version_rc_file).expect("failed to create version.rc file"); + file.write_all(version_rc_data.as_bytes()) + .expect("failed to write data to version.rc file"); + embed_resource::compile(&version_rc_file, embed_resource::NONE) + .manifest_required() + .expect("failed to compiler the Windows resource file"); + } +} + +#[cfg(not(target_os = "windows"))] +mod other { + pub(crate) fn main_stub() {} +} diff --git a/ffi/dotnet-interop-conf.toml b/ffi/dotnet-interop-conf.toml new file mode 100644 index 00000000..e2c76a3a --- /dev/null +++ b/ffi/dotnet-interop-conf.toml @@ -0,0 +1,10 @@ +namespace = "Devolutions.IronRdp" +native_lib = "DevolutionsIronRdp" + +[exceptions] +trim_suffix = "Error" +error_message_method = "ToDisplay" + +[properties] +setters_prefix = "set_" +getters_prefix = "get_" diff --git a/ffi/dotnet/.editorconfig b/ffi/dotnet/.editorconfig new file mode 100644 index 00000000..6e352c78 --- /dev/null +++ b/ffi/dotnet/.editorconfig @@ -0,0 +1,12 @@ +root = true + +# Indentation and spacing +indent_size = 4 +tab_width = 4 + +# New line preferences +end_of_line = lf +insert_final_newline = false + +[src/Picky/Generated/*.cs] +generated_code = true diff --git a/ffi/dotnet/.gitignore b/ffi/dotnet/.gitignore new file mode 100644 index 00000000..104b5441 --- /dev/null +++ b/ffi/dotnet/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/.gitignore b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/.gitignore new file mode 100644 index 00000000..a374dd6b --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/.gitignore @@ -0,0 +1,16 @@ +# Ignore directories +bin/ +obj/ + +# Ignore files +*.user +*.userosscache +*.suo +*.userprefs +*.dll +*.exe +*.pdb +*.cache +*.vsp +*.vspx +*.sap diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/App.axaml b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/App.axaml new file mode 100644 index 00000000..f807f50e --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/App.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/App.axaml.cs new file mode 100644 index 00000000..23f0f0dc --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/App.axaml.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace Devolutions.IronRdp.AvaloniaExample; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj new file mode 100644 index 00000000..802b8ad7 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj @@ -0,0 +1,27 @@ + + + WinExe + net8.0 + enable + true + app.manifest + true + true + + + + + + + + + + + + + + + + diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.sln b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.sln new file mode 100644 index 00000000..dc1a353b --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Devolutions.IronRdp.AvaloniaExample", "Devolutions.IronRdp.AvaloniaExample.csproj", "{B374556F-70F4-4B70-90AE-6DF00C532240}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B374556F-70F4-4B70-90AE-6DF00C532240}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B374556F-70F4-4B70-90AE-6DF00C532240}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B374556F-70F4-4B70-90AE-6DF00C532240}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B374556F-70F4-4B70-90AE-6DF00C532240}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1DD5DE59-5AB4-4F5F-A2AC-CA4D7012F56E} + EndGlobalSection +EndGlobal diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/KeyCodeMapper.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/KeyCodeMapper.cs new file mode 100644 index 00000000..b9482457 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/KeyCodeMapper.cs @@ -0,0 +1,104 @@ +using Avalonia.Input; +using System; +using System.Collections.Generic; + +public static class KeyCodeMapper +{ + private static readonly Dictionary KeyToScancodeMap = new Dictionary + { + {PhysicalKey.Escape, 0x01}, + {PhysicalKey.Digit1, 0x02}, + {PhysicalKey.Digit2, 0x03}, + {PhysicalKey.Digit3, 0x04}, + {PhysicalKey.Digit4, 0x05}, + {PhysicalKey.Digit5, 0x06}, + {PhysicalKey.Digit6, 0x07}, + {PhysicalKey.Digit7, 0x08}, + {PhysicalKey.Digit8, 0x09}, + {PhysicalKey.Digit9, 0x0A}, + {PhysicalKey.Digit0, 0x0B}, + {PhysicalKey.Minus, 0x0C}, + {PhysicalKey.Equal, 0x0D}, + {PhysicalKey.Backspace, 0x0E}, + {PhysicalKey.Tab, 0x0F}, + {PhysicalKey.Q, 0x10}, + {PhysicalKey.W, 0x11}, + {PhysicalKey.E, 0x12}, + {PhysicalKey.R, 0x13}, + {PhysicalKey.T, 0x14}, + {PhysicalKey.Y, 0x15}, + {PhysicalKey.U, 0x16}, + {PhysicalKey.I, 0x17}, + {PhysicalKey.O, 0x18}, + {PhysicalKey.P, 0x19}, + {PhysicalKey.BracketLeft, 0x1A}, + {PhysicalKey.BracketRight, 0x1B}, + {PhysicalKey.Enter, 0x1C}, + {PhysicalKey.ControlLeft, 0x1D}, + {PhysicalKey.A, 0x1E}, + {PhysicalKey.S, 0x1F}, + {PhysicalKey.D, 0x20}, + {PhysicalKey.F, 0x21}, + {PhysicalKey.G, 0x22}, + {PhysicalKey.H, 0x23}, + {PhysicalKey.J, 0x24}, + {PhysicalKey.K, 0x25}, + {PhysicalKey.L, 0x26}, + {PhysicalKey.Semicolon, 0x27}, + {PhysicalKey.Quote, 0x28}, + {PhysicalKey.ShiftLeft, 0x2A}, + {PhysicalKey.Backslash, 0x2B}, + {PhysicalKey.Z, 0x2C}, + {PhysicalKey.X, 0x2D}, + {PhysicalKey.C, 0x2E}, + {PhysicalKey.V, 0x2F}, + {PhysicalKey.B, 0x30}, + {PhysicalKey.N, 0x31}, + {PhysicalKey.M, 0x32}, + {PhysicalKey.Comma, 0x33}, + {PhysicalKey.Period, 0x34}, + {PhysicalKey.Slash, 0x35}, + {PhysicalKey.ShiftRight, 0x36}, + {PhysicalKey.PrintScreen, 0x37}, + {PhysicalKey.AltLeft, 0x38}, + {PhysicalKey.Space, 0x39}, + {PhysicalKey.CapsLock, 0x3A}, + {PhysicalKey.F1, 0x3B}, + {PhysicalKey.F2, 0x3C}, + {PhysicalKey.F3, 0x3D}, + {PhysicalKey.F4, 0x3E}, + {PhysicalKey.F5, 0x3F}, + {PhysicalKey.F6, 0x40}, + {PhysicalKey.F7, 0x41}, + {PhysicalKey.F8, 0x42}, + {PhysicalKey.F9, 0x43}, + {PhysicalKey.F10, 0x44}, + {PhysicalKey.NumLock, 0x45}, + {PhysicalKey.ScrollLock, 0x46}, + {PhysicalKey.Home, 0x47}, + {PhysicalKey.ArrowUp, 0x48}, + {PhysicalKey.PageUp, 0x49}, + {PhysicalKey.NumPadSubtract, 0x4A}, + {PhysicalKey.ArrowLeft, 0x4B}, + {PhysicalKey.NumPad5, 0x4C}, + {PhysicalKey.ArrowRight, 0x4D}, + {PhysicalKey.NumPadAdd, 0x4E}, + {PhysicalKey.End, 0x4F}, + {PhysicalKey.ArrowDown, 0x50}, + {PhysicalKey.PageDown, 0x51}, + {PhysicalKey.Insert, 0x52}, + {PhysicalKey.Delete, 0x53}, + {PhysicalKey.F11, 0x57}, + {PhysicalKey.F12, 0x58} + }; + + + public static ushort? GetScancode(PhysicalKey key) + { + if (KeyToScancodeMap.TryGetValue(key, out ushort scancode)) + { + return scancode; + } + return null; + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml new file mode 100644 index 00000000..0090597b --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml @@ -0,0 +1,39 @@ + + + + + + + + +

+
+ + + + diff --git a/web-client/iron-svelte-client/src/lib/messages/message-store.ts b/web-client/iron-svelte-client/src/lib/messages/message-store.ts new file mode 100644 index 00000000..9d6bf0a1 --- /dev/null +++ b/web-client/iron-svelte-client/src/lib/messages/message-store.ts @@ -0,0 +1,8 @@ +import type { Writable } from 'svelte/store'; +import { writable } from 'svelte/store'; + +type ToastMessage = { + message: string; + type: 'info' | 'error'; +}; +export const toast: Writable = writable(); diff --git a/web-client/iron-svelte-client/src/lib/messages/message.svelte b/web-client/iron-svelte-client/src/lib/messages/message.svelte new file mode 100644 index 00000000..eb1d727e --- /dev/null +++ b/web-client/iron-svelte-client/src/lib/messages/message.svelte @@ -0,0 +1,37 @@ + + +
+ info + {toastMessage} +
+ +
+ Error + {toastMessage} +
diff --git a/web-client/iron-svelte-client/src/lib/popup-screen/popup-screen.svelte b/web-client/iron-svelte-client/src/lib/popup-screen/popup-screen.svelte new file mode 100644 index 00000000..87dfb495 --- /dev/null +++ b/web-client/iron-svelte-client/src/lib/popup-screen/popup-screen.svelte @@ -0,0 +1,181 @@ + + + + + diff --git a/web-client/iron-svelte-client/src/lib/remote-screen/remote-screen.svelte b/web-client/iron-svelte-client/src/lib/remote-screen/remote-screen.svelte new file mode 100644 index 00000000..9c619373 --- /dev/null +++ b/web-client/iron-svelte-client/src/lib/remote-screen/remote-screen.svelte @@ -0,0 +1,103 @@ + + +
+
+
+ + + + + + + + + +
+ + {#if showDebugPanel} +
+ debug-panel + + +

see if text selection works correctly

+
+ {/if} +
+ +
+ + diff --git a/web-client/iron-svelte-client/src/models/session.ts b/web-client/iron-svelte-client/src/models/session.ts new file mode 100644 index 00000000..75f596b3 --- /dev/null +++ b/web-client/iron-svelte-client/src/models/session.ts @@ -0,0 +1,15 @@ +import { Guid } from 'guid-typescript'; + +export class Session { + id: Guid; + sessionId!: number; + name?: string; + active!: boolean; + desktopSize!: { width: number; height: number }; + + constructor(name?: string) { + this.id = Guid.create(); + this.name = name; + this.active = false; + } +} diff --git a/web-client/iron-svelte-client/src/routes/+layout.ts b/web-client/iron-svelte-client/src/routes/+layout.ts new file mode 100644 index 00000000..ceccaaf6 --- /dev/null +++ b/web-client/iron-svelte-client/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const prerender = true; +export const ssr = false; diff --git a/web-client/iron-svelte-client/src/routes/+page.svelte b/web-client/iron-svelte-client/src/routes/+page.svelte new file mode 100644 index 00000000..cd4ae3fd --- /dev/null +++ b/web-client/iron-svelte-client/src/routes/+page.svelte @@ -0,0 +1,4 @@ + diff --git a/web-client/iron-svelte-client/src/routes/popup-session/+page.svelte b/web-client/iron-svelte-client/src/routes/popup-session/+page.svelte new file mode 100644 index 00000000..4a69492d --- /dev/null +++ b/web-client/iron-svelte-client/src/routes/popup-session/+page.svelte @@ -0,0 +1,7 @@ + + + + diff --git a/web-client/iron-svelte-client/src/routes/session/+page.svelte b/web-client/iron-svelte-client/src/routes/session/+page.svelte new file mode 100644 index 00000000..6c4af2fb --- /dev/null +++ b/web-client/iron-svelte-client/src/routes/session/+page.svelte @@ -0,0 +1,13 @@ + + +{#if $showLogin} + +{/if} + + + diff --git a/web-client/iron-svelte-client/src/services/session.service.ts b/web-client/iron-svelte-client/src/services/session.service.ts new file mode 100644 index 00000000..c27c3ec2 --- /dev/null +++ b/web-client/iron-svelte-client/src/services/session.service.ts @@ -0,0 +1,54 @@ +import type { Guid } from 'guid-typescript'; +import type { Writable } from 'svelte/store'; +import { writable } from 'svelte/store'; +import { Session } from '../models/session'; +import type { UserInteraction } from '../../static/iron-remote-desktop'; + +export const userInteractionService: Writable = writable(); +export const currentSession: Writable = writable(); + +let _currentSession: Session = new Session('NewSession'); +let sessions: Session[] = new Array(); +let sessionCounter = 0; + +addSession('NewSession'); + +function setCurrentSession(session: Session) { + currentSession.set(session); + _currentSession = session; +} + +export function getCurrentSession(): Session { + return _currentSession; +} + +export function setCurrentSessionActive(active: boolean) { + currentSession.update((session) => { + session.active = active; + return session; + }); +} + +export function setCurrentSessionById(id: Guid) { + const session = sessions.find((session) => session.id.equals(id)); + if (session) { + setCurrentSession(session); + } +} + +export function addSession(name: string) { + sessionCounter++; + const newSession = new Session(name); + sessions.push(newSession); + if (sessionCounter == 1) { + setCurrentSession(newSession); + } +} + +export function closeSession(id: Guid) { + sessionCounter--; + sessions = sessions.filter((session) => !session.id.equals(id)); + if (sessionCounter == 1) { + setCurrentSession(sessions[0]); + } +} diff --git a/web-client/iron-svelte-client/static/beercss/beer.min.css b/web-client/iron-svelte-client/static/beercss/beer.min.css new file mode 100644 index 00000000..c92a11db --- /dev/null +++ b/web-client/iron-svelte-client/static/beercss/beer.min.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:FILL@0..1&family=Roboto:wght@300;400;500&display=swap";:root,body.light{--primary: #6750A4;--on-primary: #FFFFFF;--primary-container: #EADDFF;--on-primary-container: #21005E;--secondary: #625B71;--on-secondary: #FFFFFF;--secondary-container: #E8DEF8;--on-secondary-container: #1E192B;--tertiary: #7D5260;--on-tertiary: #FFFFFF;--tertiary-container: #FFD8E4;--on-tertiary-container: #370B1E;--error: #B3261E;--on-error: #FFFFFF;--error-container: #F9DEDC;--on-error-container: #370B1E;--background: #FFFBFE;--on-background: #1C1B1F;--surface: #FFFBFE;--on-surface: #1C1B1F;--outline: #79747E;--surface-variant: #E7E0EC;--on-surface-variant: #49454E;--inverse-surface: #313033;--inverse-on-surface: #F4EFF4;--body: #ffffff;--overlay: rgba(0,0,0,.5);--active: rgba(0,0,0,.1);--elevate1: 0 2rem 2rem 0 rgba(0, 0, 0, .14), 0 1rem 5rem 0 rgba(0, 0, 0, .12), 0 3rem 1rem -2rem rgba(0, 0, 0, .2);--elevate2: 0 6rem 10rem 0 rgba(0, 0, 0, .14), 0 1rem 18rem 0 rgba(0, 0, 0, .12), 0 3rem 5rem -1rem rgba(0, 0, 0, .3);--elevate3: 0 10rem 16rem 0 rgba(0, 0, 0, .14), 0 1rem 31rem 0 rgba(0, 0, 0, .12), 0 3rem 9rem 0rem rgba(0, 0, 0, .4);--size: 1px;--font: "Roboto", BlinkMacSystemFont, -apple-system, "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif;--speed1: .1s;--speed2: .2s;--speed3: .3s;--speed4: .4s}body.dark{--primary: #D0BCFF;--on-primary: #371E73;--primary-container: #4F378B;--on-primary-container: #EADDFF;--secondary: #CCC2DC;--on-secondary: #332D41;--secondary-container: #4A4458;--on-secondary-container: #E8DEF8;--tertiary: #EFB8C8;--on-tertiary: #492532;--tertiary-container: #633B48;--on-tertiary-container: #FFD8E4;--error: #F2B8B5;--on-error: #601410;--error-container: #8C1D18;--on-error-container: #F9DEDC;--background: #1C1B1F;--on-background: #E6E1E5;--surface: #1C1B1F;--on-surface: #E6E1E5;--outline: #938F99;--surface-variant: #49454F;--on-surface-variant: #CAC4D0;--inverse-surface: #E6E1E5;--inverse-on-surface: #313033;--body: #000000;--overlay: rgba(0,0,0,.5);--active: rgba(255,255,255,.2);--elevate1: 0 2rem 2rem 0 rgba(0, 0, 0, .14), 0 1rem 5rem 0 rgba(0, 0, 0, .12), 0 3rem 1rem -2rem rgba(0, 0, 0, .2);--elevate2: 0 6rem 10rem 0 rgba(0, 0, 0, .14), 0 1rem 18rem 0 rgba(0, 0, 0, .12), 0 3rem 5rem -1rem rgba(0, 0, 0, .3);--elevate3: 0 10rem 16rem 0 rgba(0, 0, 0, .14), 0 1rem 31rem 0 rgba(0, 0, 0, .12), 0 3rem 9rem 0rem rgba(0, 0, 0, .4);--size: 1px;--font: "Roboto", BlinkMacSystemFont, -apple-system, "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif;--speed1: .1s;--speed2: .2s;--speed3: .3s;--speed4: .4s}*{-webkit-tap-highlight-color:transparent;position:relative;vertical-align:middle;color:inherit;margin:0;padding:0;border-radius:inherit;box-sizing:border-box}body{color:var(--on-background);background-color:var(--body);overflow-x:hidden}label{font-size:12rem;vertical-align:baseline}a,b,i,span{vertical-align:bottom}a,button,.button{cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;border:none;font-family:inherit;outline:inherit;justify-content:center}.primary{background-color:var(--primary)!important;color:var(--on-primary)!important}.primary-text{color:var(--primary)!important}.primary-border{border-color:var(--primary)!important}.primary-container{background-color:var(--primary-container)!important;color:var(--on-primary-container)!important}.secondary{background-color:var(--secondary)!important;color:var(--on-secondary)!important}.secondary-text{color:var(--secondary)!important}.secondary-border{border-color:var(--secondary)!important}.secondary-container{background-color:var(--secondary-container)!important;color:var(--on-secondary-container)!important}.tertiary{background-color:var(--tertiary)!important;color:var(--on-tertiary)!important}.tertiary-text{color:var(--tertiary)!important}.tertiary-border{border-color:var(--tertiary)!important}.tertiary-container{background-color:var(--tertiary-container)!important;color:var(--on-tertiary-container)!important}.error{background-color:var(--error)!important;color:var(--on-error)!important}.error-text{color:var(--error)!important}.error-border{border-color:var(--error)!important}.error-container{background-color:var(--error-container)!important;color:var(--on-error-container)!important}.background{background-color:var(--background)!important;color:var(--on-background)!important}.surface{background-color:var(--surface)!important;color:var(--on-surface)!important}.surface-variant{background-color:var(--surface-variant)!important;color:var(--on-surface-variant)!important}.inverse-surface{background-color:var(--inverse-surface);color:var(--inverse-on-surface)}.black{background-color:#000!important}.black-border{border-color:#000!important}.black-text{color:#000!important}.white{background-color:#fff!important}.white-border{border-color:#fff!important}.white-text{color:#fff!important}.transparent{background-color:transparent!important;box-shadow:none!important;color:inherit!important}.transparent-border{border-color:transparent!important}.transparent-text{color:transparent!important}.fill:not(i){background-color:var(--surface-variant)!important;color:var(--on-surface-variant)!important}.middle-align{display:flex;align-items:center!important}.bottom-align{display:flex;align-items:flex-end!important}.top-align{display:flex;align-items:flex-start!important}.left-align{text-align:left;justify-content:flex-start!important}.right-align{text-align:right;justify-content:flex-end!important}.center-align{text-align:center;justify-content:center!important}.red,.red6{background-color:#f44336!important}.red-border{border-color:#f44336!important}.red-text{color:#f44336!important}.red1{background-color:#ffebee!important}.red2{background-color:#ffcdd2!important}.red3{background-color:#ef9a9a!important}.red4{background-color:#e57373!important}.red5{background-color:#ef5350!important}.red7{background-color:#e53935!important}.red8{background-color:#d32f2f!important}.red9{background-color:#c62828!important}.red10{background-color:#b71c1c!important}.pink,.pink6{background-color:#e91e63!important}.pink-border{border-color:#e91e63!important}.pink-text{color:#e91e63!important}.pink1{background-color:#fce4ec!important}.pink2{background-color:#f8bbd0!important}.pink3{background-color:#f48fb1!important}.pink4{background-color:#f06292!important}.pink5{background-color:#ec407a!important}.pink7{background-color:#d81b60!important}.pink8{background-color:#c2185b!important}.pink9{background-color:#ad1457!important}.pink10{background-color:#880e4f!important}.purple,.purple6{background-color:#9c27b0!important}.purple-border{border-color:#9c27b0!important}.purple-text{color:#9c27b0!important}.purple1{background-color:#f3e5f5!important}.purple2{background-color:#e1bee7!important}.purple3{background-color:#ce93d8!important}.purple4{background-color:#ba68c8!important}.purple5{background-color:#ab47bc!important}.purple7{background-color:#8e24aa!important}.purple8{background-color:#7b1fa2!important}.purple9{background-color:#6a1b9a!important}.purple10{background-color:#4a148c!important}.deep-purple,.deep-purple6{background-color:#673ab7!important}.deep-purple-border{border-color:#673ab7!important}.deep-purple-text{color:#673ab7!important}.deep-purple1{background-color:#ede7f6!important}.deep-purple2{background-color:#d1c4e9!important}.deep-purple3{background-color:#b39ddb!important}.deep-purple4{background-color:#9575cd!important}.deep-purple5{background-color:#7e57c2!important}.deep-purple7{background-color:#5e35b1!important}.deep-purple8{background-color:#512da8!important}.deep-purple9{background-color:#4527a0!important}.deep-purple10{background-color:#311b92!important}.indigo,.indigo6{background-color:#3f51b5!important}.indigo-border{border-color:#3f51b5!important}.indigo-text{color:#3f51b5!important}.indigo1{background-color:#e8eaf6!important}.indigo2{background-color:#c5cae9!important}.indigo3{background-color:#9fa8da!important}.indigo4{background-color:#7986cb!important}.indigo5{background-color:#5c6bc0!important}.indigo7{background-color:#3949ab!important}.indigo8{background-color:#303f9f!important}.indigo9{background-color:#283593!important}.indigo10{background-color:#1a237e!important}.blue,.blue6{background-color:#2196f3!important}.blue-border{border-color:#2196f3!important}.blue-text{color:#2196f3!important}.blue1{background-color:#e3f2fd!important}.blue2{background-color:#bbdefb!important}.blue3{background-color:#90caf9!important}.blue4{background-color:#64b5f6!important}.blue5{background-color:#42a5f5!important}.blue7{background-color:#1e88e5!important}.blue8{background-color:#1976d2!important}.blue9{background-color:#1565c0!important}.blue10{background-color:#0d47a1!important}.light-blue,.light-blue6{background-color:#03a9f4!important}.light-blue-border{border-color:#03a9f4!important}.light-blue-text{color:#03a9f4!important}.light-blue1{background-color:#e1f5fe!important}.light-blue2{background-color:#b3e5fc!important}.light-blue3{background-color:#81d4fa!important}.light-blue4{background-color:#4fc3f7!important}.light-blue5{background-color:#29b6f6!important}.light-blue7{background-color:#039be5!important}.light-blue8{background-color:#0288d1!important}.light-blue9{background-color:#0277bd!important}.light-blue10{background-color:#01579b!important}.cyan,.cyan6{background-color:#00bcd4!important}.cyan-border{border-color:#00bcd4!important}.cyan-text{color:#00bcd4!important}.cyan1{background-color:#e0f7fa!important}.cyan2{background-color:#b2ebf2!important}.cyan3{background-color:#80deea!important}.cyan4{background-color:#4dd0e1!important}.cyan5{background-color:#26c6da!important}.cyan7{background-color:#00acc1!important}.cyan8{background-color:#0097a7!important}.cyan9{background-color:#00838f!important}.cyan10{background-color:#006064!important}.teal,.teal6{background-color:#009688!important}.teal-border{border-color:#009688!important}.teal-text{color:#009688!important}.teal1{background-color:#e0f2f1!important}.teal2{background-color:#b2dfdb!important}.teal3{background-color:#80cbc4!important}.teal4{background-color:#4db6ac!important}.teal5{background-color:#26a69a!important}.teal7{background-color:#00897b!important}.teal8{background-color:#00796b!important}.teal9{background-color:#00695c!important}.teal10{background-color:#004d40!important}.green,.green6{background-color:#4caf50!important}.green-border{border-color:#4caf50!important}.green-text{color:#4caf50!important}.green1{background-color:#e8f5e9!important}.green2{background-color:#c8e6c9!important}.green3{background-color:#a5d6a7!important}.green4{background-color:#81c784!important}.green5{background-color:#66bb6a!important}.green7{background-color:#43a047!important}.green8{background-color:#388e3c!important}.green9{background-color:#2e7d32!important}.green10{background-color:#1b5e20!important}.light-green,.light-green6{background-color:#8bc34a!important}.light-green-border{border-color:#8bc34a!important}.light-green-text{color:#8bc34a!important}.light-green1{background-color:#f1f8e9!important}.light-green2{background-color:#dcedc8!important}.light-green3{background-color:#c5e1a5!important}.light-green4{background-color:#aed581!important}.light-green5{background-color:#9ccc65!important}.light-green7{background-color:#7cb342!important}.light-green8{background-color:#689f38!important}.light-green9{background-color:#558b2f!important}.light-green10{background-color:#33691e!important}.lime,.lime6{background-color:#cddc39!important}.lime-border{border-color:#cddc39!important}.lime-text{color:#cddc39!important}.lime1{background-color:#f9fbe7!important}.lime2{background-color:#f0f4c3!important}.lime3{background-color:#e6ee9c!important}.lime4{background-color:#dce775!important}.lime5{background-color:#d4e157!important}.lime7{background-color:#c0ca33!important}.lime8{background-color:#afb42b!important}.lime9{background-color:#9e9d24!important}.lime10{background-color:#827717!important}.yellow,.yellow6{background-color:#ffeb3b!important}.yellow-border{border-color:#ffeb3b!important}.yellow-text{color:#ffeb3b!important}.yellow1{background-color:#fffde7!important}.yellow2{background-color:#fff9c4!important}.yellow3{background-color:#fff59d!important}.yellow4{background-color:#fff176!important}.yellow5{background-color:#ffee58!important}.yellow7{background-color:#fdd835!important}.yellow8{background-color:#fbc02d!important}.yellow9{background-color:#f9a825!important}.yellow10{background-color:#f57f17!important}.amber,.amber6{background-color:#ffc107!important}.amber-border{border-color:#ffc107!important}.amber-text{color:#ffc107!important}.amber1{background-color:#fff8e1!important}.amber2{background-color:#ffecb3!important}.amber3{background-color:#ffe082!important}.amber4{background-color:#ffd54f!important}.amber5{background-color:#ffca28!important}.amber7{background-color:#ffb300!important}.amber8{background-color:#ffa000!important}.amber9{background-color:#ff8f00!important}.amber10{background-color:#ff6f00!important}.orange,.orange6{background-color:#ff9800!important}.orange-border{border-color:#ff9800!important}.orange-text{color:#ff9800!important}.orange1{background-color:#fff3e0!important}.orange2{background-color:#ffe0b2!important}.orange3{background-color:#ffcc80!important}.orange4{background-color:#ffb74d!important}.orange5{background-color:#ffa726!important}.orange7{background-color:#fb8c00!important}.orange8{background-color:#f57c00!important}.orange9{background-color:#ef6c00!important}.orange10{background-color:#e65100!important}.deep-orange,.deep-orange6{background-color:#ff5722!important}.deep-orange-border{border-color:#ff5722!important}.deep-orange-text{color:#ff5722!important}.deep-orange1{background-color:#fbe9e7!important}.deep-orange2{background-color:#ffccbc!important}.deep-orange3{background-color:#ffab91!important}.deep-orange4{background-color:#ff8a65!important}.deep-orange5{background-color:#ff7043!important}.deep-orange7{background-color:#f4511e!important}.deep-orange8{background-color:#e64a19!important}.deep-orange9{background-color:#d84315!important}.deep-orange10{background-color:#bf360c!important}.brown,.brown6{background-color:#795548!important}.brown-border{border-color:#795548!important}.brown-text{color:#795548!important}.brown1{background-color:#efebe9!important}.brown2{background-color:#d7ccc8!important}.brown3{background-color:#bcaaa4!important}.brown4{background-color:#a1887f!important}.brown5{background-color:#8d6e63!important}.brown7{background-color:#6d4c41!important}.brown8{background-color:#5d4037!important}.brown9{background-color:#4e342e!important}.brown10{background-color:#3e2723!important}.blue-grey,.blue-grey6{background-color:#607d8b!important}.blue-grey-border{border-color:#607d8b!important}.blue-grey-text{color:#607d8b!important}.blue-grey1{background-color:#eceff1!important}.blue-grey2{background-color:#cfd8dc!important}.blue-grey3{background-color:#b0bec5!important}.blue-grey4{background-color:#90a4ae!important}.blue-grey5{background-color:#78909c!important}.blue-grey7{background-color:#546e7a!important}.blue-grey8{background-color:#455a64!important}.blue-grey9{background-color:#37474f!important}.blue-grey10{background-color:#263238!important}.grey,.grey6{background-color:#9e9e9e!important}.grey-border{border-color:#9e9e9e!important}.grey-text{color:#9e9e9e!important}.grey1{background-color:#fafafa!important}.grey2{background-color:#f5f5f5!important}.grey3{background-color:#eee!important}.grey4{background-color:#e0e0e0!important}.grey5{background-color:#bdbdbd!important}.grey7{background-color:#757575!important}.grey8{background-color:#616161!important}.grey9{background-color:#424242!important}.grey10{background-color:#212121!important}.horizontal{display:inline-flex;flex-direction:row!important;gap:16rem;width:auto!important;max-width:none!important}.horizontal>*{margin-top:0!important;margin-bottom:0!important}.vertical,button.vertical,.button.vertical,.chip.vertical{display:inline-flex;flex-direction:column!important;gap:4rem;height:auto!important;max-height:none!important;padding-top:8rem;padding-bottom:8rem}.vertical>*{margin-left:0!important;margin-right:0!important}.divider,.small-divider,.medium-divider,.large-divider{border-bottom:1rem solid var(--outline);display:block;margin:0!important}.medium-divider{margin:16rem 0!important}.large-divider{margin:24rem 0!important}.small-divider{margin:8rem 0!important}.no-elevate{box-shadow:none!important}.small-elevate,.elevate{box-shadow:var(--elevate1)!important}.medium-elevate{box-shadow:var(--elevate2)!important}.large-elevate{box-shadow:var(--elevate3)!important}.round:not(img,video){border-radius:32rem!important}.no-round{border-radius:0!important}.top-round{border-radius:32rem 32rem 0 0!important}.bottom-round{border-radius:0 0 32rem 32rem!important}.left-round{border-radius:32rem 0 0 32rem!important}.right-round{border-radius:0 32rem 32rem 0!important}.top-round.left-round{border-radius:32rem 32rem 0!important}.top-round.right-round{border-radius:32rem 32rem 32rem 0!important}.bottom-round.left-round{border-radius:32rem 0 32rem 32rem!important}.bottom-round.right-round{border-radius:0 32rem 32rem!important}.circle:not(img,video),.square:not(img,video){height:40rem;width:40rem;padding:0}.circle>span,.square>span{display:none}.square.small:not(img,video),.circle.small:not(img,video){height:32rem;width:32rem}.square.large:not(img,video),.circle.large:not(img,video){height:48rem;width:48rem}.square.extra:not(img,video),.circle.extra:not(img,video){height:56rem;width:56rem}.square.round,.circle.round{border-radius:16rem!important}.border:not(table,.field){box-sizing:border-box;border:1rem solid var(--outline);background-color:transparent;box-shadow:none}.no-margin{margin:0!important}.tiny-margin{padding:4rem!important}.small-margin{margin:8rem!important}.medium-margin,.margin{margin:16rem!important}.large-margin{margin:24rem!important}.no-opacity{opacity:0}.opacity{opacity:1}.no-padding{padding:0!important}.tiny-padding{padding:4rem!important}.small-padding{padding:8rem!important}.medium-padding,.padding{padding:16rem!important}.large-padding{padding:24rem!important}.front{z-index:10!important}.back{z-index:-10!important}.left{left:0}.right{right:0}.top{top:0}.bottom{bottom:0}.center{left:50%;transform:translate(-50%)}.middle{top:50%;transform:translateY(-50%)}.middle.center{transform:translate(-50%,-50%)}.scroll{overflow:auto;padding-bottom:16rem}.no-scroll{overflow:hidden}.shadow{background-color:#00000050!important}.left-shadow{background-color:transparent!important;background-image:linear-gradient(to right,black,transparent)}.right-shadow{background-color:transparent!important;background-image:linear-gradient(to left,black,transparent)}.bottom-shadow{background-color:transparent!important;background-image:linear-gradient(to top,black,transparent)}.top-shadow{background-color:transparent!important;background-image:linear-gradient(to bottom,black,transparent)}.small-width{width:192rem!important;max-width:100%}.medium-width{width:384rem!important;max-width:100%}.large-width{width:576rem!important;max-width:100%}.small-height{height:192rem!important}.medium-height{height:384rem!important}.large-height{height:576rem!important}.wrap{display:block;white-space:normal}.no-wrap:not(.dropdown){display:flex;white-space:nowrap}.tiny-space:not(nav,.row,.grid,table){height:8rem}.space:not(nav,.row,.grid,table),.small-space:not(nav,.row,.grid,table){height:16rem}.medium-space:not(nav,.row,.grid,table){height:32rem}.large-space:not(nav,.row,.grid,table){height:48rem}.responsive{width:-webkit-fill-available;width:-moz-available}@media only screen and (max-width: 600px){.m:not(.s),.l:not(.s),.m.l:not(.s){display:none}}@media only screen and (min-width: 601px) and (max-width: 992px){.s:not(.m),.l:not(.m),.s.l:not(.m){display:none}}@media only screen and (min-width: 993px){.m:not(.l),.s:not(.l),.m.s:not(.l){display:none}}html{font-size:var(--size)}body{font-family:var(--font);font-size:14rem}h1,h2,h3,h4,h5,h6{font-weight:400;display:flex;align-items:center;margin-bottom:8rem}*+h1,*+h2,*+h3,*+h4,*+h5,*+h6{margin-top:16rem}h1{font-size:96rem}h2{font-size:60rem}h3{font-size:48rem}h4{font-size:34rem}h5{font-size:24rem}h6{font-size:20rem}.link{color:var(--primary)!important}.truncate{overflow:hidden;white-space:nowrap!important;text-overflow:ellipsis}.truncate>*{white-space:nowrap!important}.small-text{font-size:12rem}.medium-text{font-size:14rem}.large-text{font-size:16rem}.upper{text-transform:uppercase}.lower{text-transform:lowercase}.capitalize{text-transform:capitalize}.bold{font-weight:700}.overline{text-decoration:line-through}.underline{text-decoration:underline}.italic{font-style:italic}p{margin:8rem 0}.wave:after,.wave.light:after,.chip:after,.button:after,button:after{content:"";position:absolute;top:0;left:0;z-index:1;border-radius:inherit;width:100%;height:100%;background-position:center;background-image:radial-gradient(circle,rgba(255,255,255,.4) 1%,transparent 1%);opacity:0;transition:none}.wave.dark:after,.button.none:after,button.none:after,.chip.border:after,.button.border:after,button.border:after,.chip.transparent:after,.button.transparent:after,button.transparent:after{background-image:radial-gradient(circle,rgba(150,150,150,.2) 1%,transparent 1%)}.wave.none:after,.button.none:after,button.none:after{top:0;left:-4rem;padding:0 4rem}.wave.none:not(.small,.medium,.large,.extra):after,.button.none:not(.small,.medium,.large,.extra):after,button.none:not(.small,.medium,.large,.extra):after{top:-4rem;left:-4rem;padding:4rem}.wave:hover:after,.chip:hover:after,.button:hover:after,button:hover:after,.wave:focus:after,.chip:focus:after,.button:focus:after,button:focus:after{background-size:15000%;opacity:1;transition:var(--speed2) background-size linear}.wave:active:after,.chip:active:after,.button:active:after,button:active:after{background-size:5000%;opacity:0;transition:none}.no-wave:after,.no-wave:hover:after,.no-wave:active:after{display:none}.badge{display:inline-flex;align-items:center;justify-content:center;position:absolute;font-size:12rem;text-transform:none;z-index:1;padding:0 6rem;background-color:var(--error);color:var(--on-error);top:0;left:auto;bottom:auto;right:0;transform:translate(50%,-100%);height:20rem}.badge.none{top:auto;left:auto;bottom:auto;right:auto;transform:none;position:relative;margin:0 2rem}.badge.top{top:0;left:50%;bottom:auto;right:auto;transform:translate(-50%,-100%)}.badge.bottom{top:auto;left:50%;bottom:0;right:auto;transform:translate(-50%,100%)}.badge.left{top:50%;left:0;bottom:auto;right:auto;transform:translate(-100%,-50%)}.badge.right{top:50%;left:auto;bottom:auto;right:0;transform:translate(100%,-50%)}.badge.top.left{top:0;left:0;bottom:auto;right:auto;transform:translate(-50%,-100%)}.badge.top.right{top:0;left:auto;bottom:auto;right:0;transform:translate(50%,-100%)}.badge.bottom.left{top:auto;left:0;bottom:0;right:auto;transform:translate(-50%,100%)}.badge.bottom.right{top:auto;left:auto;bottom:0;right:0;transform:translate(50%,100%)}.badge.border{border:1rem solid var(--error);color:var(--error)}.badge.circle,.badge.square{padding:0;text-align:center;width:20rem;height:20rem}.badge.circle{border-radius:50%}.badge.square{border-radius:0}.button,button{box-sizing:content-box;display:inline-flex;align-items:center;justify-content:center;height:40rem;min-width:40rem;font-size:14rem;font-weight:500;color:var(--on-primary);padding:0 24rem;background-color:var(--primary);margin:0 8rem;border-radius:8rem;transition:var(--speed3) transform,var(--speed3) border-radius,var(--speed3) padding;user-select:none;gap:16rem}.button.none,button.none{width:auto;height:auto;color:var(--primary);padding:0;background-color:transparent;min-width:auto;min-height:24rem}.button.small,button.small{height:32rem;min-width:32rem;font-size:14rem}.button.large,button.large{height:48rem;min-width:48rem}.button.extra,button.extra,.button.extend,button.extend{height:56rem;min-width:56rem;font-size:16rem}.button.border,button.border{border:1rem solid var(--primary);color:var(--primary)}.button.circle,button.circle{border-radius:40rem;padding:0}.button.square,button.square{border-radius:0;padding:0}.button.extend,button.extend{padding:0}.button.extend>span,button.extend>span{display:none}.button.extend:hover,button.extend:hover,.button.extend.active,button.extend.active{width:auto;padding:0 16rem}.button.extend:hover>i+span,button.extend:hover>i+span,.button.extend.active>i+span,button.extend.active>i+span{display:inherit;margin-left:32rem}.button.extend:hover>img+span,button.extend:hover>img+span,.button.extend.active>img+span,button.extend.active>img+span{display:inherit;margin-left:48rem}.button[disabled],button:disabled{opacity:.5;cursor:not-allowed}.button[disabled]{pointer-events:none}.button[disabled]:before,button:disabled:before,.button[disabled]:after,button:disabled:after{display:none}.button.fill,button.fill{background-color:var(--secondary-container)!important;color:var(--on-secondary-container)!important}article{box-shadow:var(--elevate1);background-color:var(--surface);color:var(--on-surface);padding:16rem;border-radius:12rem;display:block;transition:var(--speed3) transform,var(--speed3) border-radius,var(--speed3) padding}*+article{margin-top:16rem}article.small{height:192rem}article.medium{height:320rem}article.large{height:512rem}.chip{box-sizing:content-box;display:inline-flex;align-items:center;justify-content:center;height:40rem;min-width:40rem;font-size:14rem;font-weight:500;color:var(--on-secondary);padding:0 16rem;background-color:var(--secondary);margin:0 8rem;text-transform:none;border-radius:8rem;transition:var(--speed3) transform,var(--speed3) border-radius,var(--speed3) padding;user-select:none;gap:16rem}.chip.small{height:32rem;min-width:32rem}.chip.large{height:48rem;min-width:48rem}.chip.border{border:1rem solid var(--secondary);color:var(--secondary)}.chip.circle{border-radius:40rem;padding:0}.chip.square{border-radius:0;padding:0}.chip.fill{background-color:var(--secondary-container)!important;color:var(--on-secondary-container)!important;border:none}main.responsive{margin:0 auto;max-width:1200rem;padding:8rem;overflow-x:hidden;min-height:100vh}main.responsive.max{max-width:100%}nav.bottom:not(.s,.m,.l)~.responsive:not(header){padding-bottom:96rem}nav.top:not(.s,.m,.l)~.responsive:not(footer){padding-top:96rem}nav.left:not(.s,.m,.l)~.responsive{padding-left:96rem}nav.right:not(.s,.m,.l)~.responsive{padding-right:96rem}@media only screen and (max-width: 600px){nav.s.bottom~.responsive:not(header){padding-bottom:96rem}nav.s.top~.responsive:not(footer){padding-top:96rem}nav.s.left~.responsive{padding-left:96rem}nav.s.right~.responsive{padding-right:96rem}}@media only screen and (min-width: 601px) and (max-width: 992px){nav.m.bottom~.responsive:not(header){padding-bottom:96rem}nav.m.top~.responsive:not(footer){padding-top:96rem}nav.m.left~.responsive{padding-left:96rem}nav.m.right~.responsive{padding-right:96rem}}@media only screen and (min-width: 993px){nav.l.bottom~.responsive:not(header){padding-bottom:96rem}nav.l.top~.responsive:not(footer){padding-top:96rem}nav.l.left~.responsive{padding-left:96rem}nav.l.right~.responsive{padding-right:96rem}}@media only screen and (max-width: 600px){main.responsive{padding-right:8rem;padding-left:8rem}}.dropdown{opacity:0;visibility:hidden;position:absolute;box-shadow:var(--elevate2);background-color:var(--surface);z-index:11;top:auto;bottom:0;left:0;right:auto;width:100%;max-height:50vh;max-width:none;transform:translateY(100%);overflow-x:hidden;overflow-y:auto;font-size:14rem;font-weight:400;text-transform:none;color:var(--on-surface);line-height:normal;text-align:left;border-radius:4rem;transform:scale(.8) translateY(120%);transition:var(--speed2) all,0s background-color}.dropdown.no-wrap{width:auto;white-space:nowrap!important}.dropdown.active,.dropdown:not([data-ui]):active,button:not([data-ui]):focus-within>.dropdown,.button:not([data-ui]):focus-within>.dropdown,.field>:not([data-ui]):focus-within~.dropdown{opacity:1;visibility:visible;transform:scale(1) translateY(100%)}.dropdown.left{left:auto;right:0}.dropdown *{white-space:inherit!important}.dropdown>a{padding:12rem 16rem;border-radius:0}.dropdown>a:not(.row){display:block}.dropdown>a:hover,.dropdown>a:focus,.dropdown>a.active{background-color:var(--active)}summary.none{list-style-type:none}summary{cursor:pointer}summary:focus{outline:none}.field{height:48rem;margin-bottom:32rem}*+.field{margin-top:16rem}.grid>*>.field{margin-bottom:16rem}.grid>*>.field+.field{margin-top:32rem}.grid.no-space>*>.field+.field{margin-top:16rem}.grid.medium-space>*>.field+.field{margin-top:40rem}.grid.large-space>*>.field+.field{margin-top:48rem}.field.small{height:40rem}.field.medium{height:48rem}.field.large{height:56rem}.field.extra{height:64rem}.field:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;border-radius:inherit;background-color:inherit}.field.fill:before{background-color:var(--surface-variant);color:var(--on-surface-variant)}.field>i,.field>img,.field>.loader{position:absolute;top:50%;left:auto;right:16rem;transform:translateY(-50%);cursor:pointer;z-index:0}.field.border>i,.field.fill>i,.field.round>i,.field.border>img,.field.fill>img,.field.round>img,.field.border>.loader,.field.fill>.loader,.field.round>.loader{left:auto;right:16rem}.field>i:first-child,.field>img:first-child,.field>.loader:first-child{left:16rem;right:auto}.field.border>i:first-child,.field.fill>i:first-child,.field.round>i:first-child,.field.border>img:first-child,.field.fill>img:first-child,.field.round>img:first-child,.field.border>.loader:first-child,.field.fill>.loader:first-child,.field.round>.loader:first-child{left:16rem;right:auto}.field.invalid>i{color:var(--error)}.field>.loader{border-width:3rem;width:24rem;height:24rem}.field>select,input[type^=date],input[type^=time]{-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer}input[type^=date]::-webkit-inner-spin-button,input[type^=date]::-webkit-calendar-picker-indicator,input[type^=time]::-webkit-inner-spin-button,input[type^=time]::-webkit-calendar-picker-indicator{opacity:0;position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:100%;z-index:-1;cursor:pointer}input[type=file]{position:absolute;top:0;left:0;width:100%;height:100%;z-index:2;opacity:0;cursor:pointer}.field>input,.field>textarea,.field>select{border:1rem solid transparent;padding:0 15rem;font-family:inherit;font-size:16rem;width:100%;height:100%;outline:none;z-index:1;background:none;resize:none}.field>input:focus,.field>textarea:focus,.field>select:focus{border:2rem solid transparent;padding:0 14rem}.field.border>input,.field.border>textarea,.field.border>select{border-color:var(--outline)}.field.border>input:focus,.field.border>textarea:focus,.field.border>select:focus{border-color:var(--primary)}.field.round>input,.field.round>textarea,.field.round>select{padding-left:23rem;padding-right:23rem}.field.round>input:focus,.field.round>textarea:focus,.field.round>select:focus{padding-left:22rem;padding-right:22rem}.field.prefix>input,.field.prefix>textarea,.field.prefix>select{padding-left:47rem}.field.prefix>input:focus,.field.prefix>textarea:focus,.field.prefix>select:focus{padding-left:46rem}.field.suffix>input,.field.suffix>textarea,.field.suffix>select{padding-right:47rem}.field.suffix>input:focus,.field.suffix>textarea:focus,.field.suffix>select:focus{padding-right:46rem}.field:not(.border,.round)>input,.field:not(.border,.round)>textarea,.field:not(.border,.round)>select{border-bottom-color:var(--outline)}.field:not(.border,.round)>input:focus,.field:not(.border,.round)>textarea:focus,.field:not(.border,.round)>select:focus{border-bottom-color:var(--primary)}.field{border-radius:4rem 4rem 0 0}.field.border{border-radius:4rem}.field.round{border-radius:32rem}.field.round:not(.border,.fill)>input,.field.round:not(.border,.fill)>textarea,.field.round:not(.border,.fill)>select,.field.round:not(.border)>input:focus,.field.round:not(.border)>textarea:focus,.field.round:not(.border)>select:focus{box-shadow:var(--elevate1)}.field.round:not(.border,.fill)>input:focus,.field.round:not(.border,.fill)>textarea:focus,.field.round:not(.border,.fill)>select:focus{box-shadow:var(--elevate2)}.field.invalid:not(.border,.round)>input,.field.invalid:not(.border,.round)>textarea,.field.invalid:not(.border,.round)>select,.field.invalid:not(.border,.round)>input:focus,.field.invalid:not(.border,.round)>textarea:focus,.field.invalid:not(.border,.round)>select:focus{border-bottom-color:var(--error)}.field.invalid.border>input,.field.invalid.border>textarea,.field.invalid.border>select,.field.invalid.border>input:focus,.field.invalid.border>textarea:focus,.field.invalid.border>select:focus{border-color:var(--error)}.field>:disabled{opacity:.5;cursor:not-allowed}.field.small.textarea{height:72rem}.field.textarea,.field.medium.textarea{height:88rem}.field.large.textarea{height:104rem}.field.extra.textarea{height:120rem}.field>select>option{background-color:var(--surface);color:var(--on-surface)}.field.label>input,.field.label>select{padding-top:16rem}.field.label.border:not(.fill)>input,.field.label.border:not(.fill)>select{padding-top:0}.field.label.small>textarea{padding-top:18rem}.field.label>textarea,.field.label.medium>textarea{padding-top:22rem}.field.label.large>textarea{padding-top:26rem}.field.label.extra>textarea{padding-top:30rem}.field.small:not(.label)>textarea,.field.small.border:not(.fill)>textarea{padding-top:10rem}.field:not(.label)>textarea,.field.border:not(.fill)>textarea,.field.medium:not(.label)>textarea,.field.medium.border:not(.fill)>textarea{padding-top:14rem}.field.large:not(.label)>textarea,.field.large.border:not(.fill)>textarea{padding-top:18rem}.field.extra:not(.label)>textarea,.field.extra.border:not(.fill)>textarea{padding-top:22rem}.field.label>label{position:absolute;top:50%;left:16rem;font-size:16rem;transform:translateY(-50%);transition:var(--speed2) all,0s background-color;z-index:0}.field.label.textarea.small>label{top:20rem}.field.label.textarea>label,.field.label.textarea.medium>label{top:24rem}.field.label.textarea.large>label{top:28rem}.field.label.textarea.extra>label{top:32rem}.field.label.round>label{left:24rem}.field.label.prefix>label{left:48rem}.field.label>label.active,.field.label>[placeholder]:focus~label,.field.label>[placeholder]:not(:placeholder-shown)~label{font-size:12rem;transform:translateY(-120%);z-index:1}.field.label.border:not(.fill)>label.active,.field.label.border:not(.fill)>[placeholder]:focus~label,.field.label.border:not(.fill)>[placeholder]:not(:placeholder-shown)~label{font-size:12rem;top:0%;left:16rem;transform:translateY(-50%);z-index:1}.field.label.border.round:not(.fill)>label.active,.field.label.border.round:not(.fill)>[placeholder]:focus~label,.field.label.border.round:not(.fill)>[placeholder]:not(:placeholder-shown)~label{left:24rem;z-index:1}.field.label>:focus~label{color:var(--primary)}.field.invalid>label{color:var(--error)!important}.field>label.required:after,.field.required>label:after{content:" * "}.field>.helper,.field>.error{position:absolute;left:16rem;bottom:0;transform:translateY(100%);font-size:12rem;background:none!important;padding-top:2rem}a.helper{color:var(--primary)}.field>.error{color:var(--error)!important}.field.round>.helper,.field.round>.error{left:24rem}.field.invalid>.helper{display:none}table td>.field{max-height:100%;height:100%}.grid{--grid-gap: 16rem;display:grid;grid-template-columns:repeat(12,calc(8.33% - var(--grid-gap) + (var(--grid-gap)/12)));gap:var(--grid-gap)}*+.grid{margin-top:16rem}.grid.no-space{--grid-gap: 0rem}.grid.medium-space{--grid-gap: 24rem}.grid.large-space{--grid-gap: 32rem}.s1{grid-area:auto/span 1}.s2{grid-area:auto/span 2}.s3{grid-area:auto/span 3}.s4{grid-area:auto/span 4}.s5{grid-area:auto/span 5}.s6{grid-area:auto/span 6}.s7{grid-area:auto/span 7}.s8{grid-area:auto/span 8}.s9{grid-area:auto/span 9}.s10{grid-area:auto/span 10}.s11{grid-area:auto/span 11}.s12{grid-area:auto/span 12}@media only screen and (min-width: 601px){.m1{grid-area:auto/span 1}.m2{grid-area:auto/span 2}.m3{grid-area:auto/span 3}.m4{grid-area:auto/span 4}.m5{grid-area:auto/span 5}.m6{grid-area:auto/span 6}.m7{grid-area:auto/span 7}.m8{grid-area:auto/span 8}.m9{grid-area:auto/span 9}.m10{grid-area:auto/span 10}.m11{grid-area:auto/span 11}.m12{grid-area:auto/span 12}}@media only screen and (min-width: 993px){.l1{grid-area:auto/span 1}.l2{grid-area:auto/span 2}.l3{grid-area:auto/span 3}.l4{grid-area:auto/span 4}.l5{grid-area:auto/span 5}.l6{grid-area:auto/span 6}.l7{grid-area:auto/span 7}.l8{grid-area:auto/span 8}.l9{grid-area:auto/span 9}.l10{grid-area:auto/span 10}.l11{grid-area:auto/span 11}.l12{grid-area:auto/span 12}}i{font-family:Material Symbols Outlined;font-weight:400;font-style:normal;font-size:24rem;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:"liga";-webkit-font-smoothing:antialiased;vertical-align:middle;text-align:center;overflow:hidden;width:24rem;min-width:24rem;box-sizing:content-box}i.tiny{font-size:16rem;width:16rem;min-width:16rem}i.small{font-size:20rem;width:20rem;min-width:20rem}i.large{font-size:28rem;width:28rem;min-width:28rem}i.extra{font-size:32rem;width:32rem}i.fill,a.row:hover>i,a.row:focus>i,.transparent:hover>i,.transparent:focus>i{font-variation-settings:"FILL" 1}.absolute{position:absolute}.fixed{position:fixed}.absolute.left.right,.fixed.left.right{width:auto}.absolute.left.right.small,.fixed.left.right.small{height:320rem}.absolute.left.right.medium,.fixed.left.right.medium{height:448rem}.absolute.left.right.large,.fixed.left.right.large{height:704rem}.absolute.top.bottom.small,.fixed.top.bottom.small{width:320rem}.absolute.top.bottom.medium,.fixed.top.bottom.medium{width:448rem}.absolute.top.bottom.large,.fixed.top.bottom.large{width:704rem}header,footer{padding:0 16rem;background-color:var(--surface)}header.fixed,footer.fixed{position:sticky;top:0;bottom:0;left:0;right:0;z-index:12;background-color:inherit;border-radius:0}header.fixed:before,footer.fixed:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;margin:0 -8rem;max-width:100%;background-color:inherit}.modal>header,.modal>footer{padding:0}article>header,article>footer{padding:inherit;padding-left:0;padding-right:0;z-index:11!important}.modal>header.fixed,article>header.fixed,.padding>header.fixed,.medium-padding>header.fixed{transform:translateY(-16rem)}.modal>footer.fixed,article>footer.fixed,.padding>footer.fixed,.medium-padding>footer.fixed{transform:translateY(16rem)}.no-padding>header.fixed,.no-padding>footer.fixed{transform:none}.small-padding>header.fixed{transform:translateY(-8rem)}.small-padding>footer.fixed{transform:translateY(8rem)}.large-padding>header.fixed{transform:translateY(-24rem)}.large-padding>footer.fixed{transform:translateY(24rem)}.loader{--loader-translateY: 0;display:inline-block;width:40rem;height:40rem;border-radius:50%;border:4rem solid var(--primary);clip-path:polygon(50% 50%,0% 0%,50% 0%,50% 0%,50% 0%,50% 0%,50% 0%,50% 0%,50% 0%);animation:1.6s to-loader linear infinite;background:none!important}.loader.small{width:24rem;height:24rem;border-width:3rem}.loader.large{width:56rem;height:56rem;border-width:5rem}.loader.red{border-color:#f44336}.loader.pink{border-color:#e91e63}.loader.purple{border-color:#9c27b0}.loader.deep-purple{border-color:#673ab7}.loader.indigo{border-color:#3f51b5}.loader.blue{border-color:#2196f3}.loader.light-blue{border-color:#03a9f4}.loader.cyan{border-color:#00bcd4}.loader.teal{border-color:#009688}.loader.green{border-color:#4caf50}.loader.light-green{border-color:#8bc34a}.loader.lime{border-color:#cddc39}.loader.yellow{border-color:#ffeb3b}.loader.amber{border-color:#ffc107}.loader.orange{border-color:#ff9800}.loader.deep-orange{border-color:#ff5722}.loader.brown{border-color:#795548}.loader.blue-grey{border-color:#607d8b}.loader.grey{border-color:#9e9e9e}.loader.black{border-color:#000}.loader.white{border-color:#fff}.field>.loader{--loader-translateY: -50%}@keyframes to-loader{0%{transform:translateY(var(--loader-translateY)) rotate(0);clip-path:polygon(50% 50%,0% 0%,50% 0%,50% 0%,50% 0%,50% 0%,50% 0%,50% 0%,50% 0%)}20%{clip-path:polygon(50% 50%,0% 0%,50% 0%,100% 0%,100% 50%,100% 50%,100% 50%,100% 50%,100% 50%)}30%{clip-path:polygon(50% 50%,0% 0%,50% 0%,100% 0%,100% 50%,100% 100%,50% 100%,50% 100%,50% 100%)}40%{clip-path:polygon(50% 50%,0% 0%,50% 0%,100% 0%,100% 50%,100% 100%,50% 100%,0% 100%,0% 50%)}50%{clip-path:polygon(50% 50%,50% 0%,50% 0%,100% 0%,100% 50%,100% 100%,50% 100%,0% 100%,0% 50%)}60%{clip-path:polygon(50% 50%,100% 50%,100% 50%,100% 50%,100% 50%,100% 100%,50% 100%,0% 100%,0% 50%)}70%{clip-path:polygon(50% 50%,50% 100%,50% 100%,50% 100%,50% 100%,50% 100%,50% 100%,0% 100%,0% 50%)}80%{clip-path:polygon(50% 50%,0% 100%,0% 100%,0% 100%,0% 100%,0% 100%,0% 100%,0% 100%,0% 50%)}90%{transform:translateY(var(--loader-translateY)) rotate(360deg);clip-path:polygon(50% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%)}to{clip-path:polygon(50% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%,0% 50%)}}img.small,img.medium,img.large,img.tiny,img.extra,img.round,img.circle,img.responsive,video.small,video.medium,video.large,video.tiny,video.extra,video.round,video.circle,video.responsive{object-fit:cover;object-position:center;transition:var(--speed3) transform,var(--speed3) border-radius,var(--speed3) padding;height:48rem;width:48rem}img.round,video.round{border-radius:8rem}img.circle,video.circle{border-radius:50%}img.tiny,video.tiny{height:32rem;width:32rem}img.small,video.small{height:40rem;width:40rem}img.large,video.large{height:56rem;width:56rem}img.extra,video.extra{height:64rem;width:64rem}img.responsive,video.responsive{width:100%;height:100%;margin:0 auto}button>img.responsive,.button>img.responsive,.chip>img.responsive{width:40rem}button:not(.transparent)>img.responsive,.button:not(.transparent)>img.responsive,.chip:not(.transparent)>img.responsive{border:4rem solid transparent}button.small>img.responsive,.button.small>img.responsive,.chip.small>img.responsive{width:32rem}button.large>img.responsive,.button.large>img.responsive,.chip.large>img.responsive{width:48rem}button.extra>img.responsive,.button.extra>img.responsive,.chip.extra>img.responsive{width:56rem}img.responsive.tiny,video.responsive.tiny{width:100%;height:64rem}img.responsive.small,video.responsive.small{width:100%;height:128rem}img.responsive.medium,video.responsive.medium{width:100%;height:192rem}img.responsive.large,video.responsive.large{width:100%;height:256rem}img.responsive.extra,video.responsive.extra{width:100%;height:320rem}img.responsive.round,video.responsive.round{border-radius:32rem}img.empty-state,video.empty-state{max-width:100%;width:384rem}button>img:not(.responsive,.tiny,.small,.medium,.large,.extra),.button>img:not(.responsive,.tiny,.small,.medium,.large,.extra),.chip>img:not(.responsive,.tiny,.small,.medium,.large,.extra),.field>img:not(.responsive,.tiny,.small,.medium,.large,.extra),.tabs img:not(.responsive,.tiny,.small,.medium,.large,.extra),td img:not(.responsive,.tiny,.small,.medium,.large,.extra){min-width:24rem;max-width:24rem;min-height:24rem;max-height:24rem}.button>i,.button>img,.button>img.responsive,button>i,button>img,button>img.responsive,.chip>i,.chip>img,.chip>img.responsive{margin:0 -8rem}.button>img.responsive,button>img.responsive{margin-left:-24rem}.button>span+img.responsive,button>span+img.responsive{margin-right:-24rem}.chip>img.responsive{margin-left:-16rem}.chip>span+img.responsive{margin-right:-16rem}.circle>img.responsive,.square>img.responsive{margin:0}.extend>i,.extend>img{margin:0;position:absolute;left:16rem}.extend>img.responsive{left:0;width:56rem}.extend.border>img.responsive{width:54rem}.modal{opacity:0;visibility:hidden;position:fixed;box-shadow:var(--elevate2);color:var(--on-surface);background-color:var(--surface);padding:16rem;z-index:100;left:50%;top:10%;min-width:320rem;max-width:100%;max-height:80%;overflow-x:hidden;overflow-y:auto;transition:var(--speed3) all,0s background-color;transform:translate(-50%,-64rem)}.modal:not(.left,.right,.top,.bottom){border-radius:12rem}.modal.small{width:25%;height:25%}.modal.medium{width:50%;height:50%}.modal.large{width:75%;height:75%}.modal.active{opacity:1;visibility:visible;transform:translate(-50%)}.modal.active.left,.modal.active.right,.modal.active.top,.modal.active.bottom,.modal.active.max{transform:translate(0)}.modal.top{opacity:1;top:0;left:0;right:auto;bottom:auto;height:auto;width:100%;min-width:auto;max-height:100%;transform:translateY(-100%)}.modal.left{opacity:1;top:0;left:0;right:auto;bottom:auto;width:auto;height:100%;max-height:100%;transform:translate(-100%)}.modal.right{opacity:1;top:0;left:auto;right:0;bottom:auto;width:auto;height:100%;max-height:100%;transform:translate(100%)}.modal.bottom{opacity:1;top:auto;left:0;right:auto;bottom:0;height:auto;width:100%;min-width:auto;max-height:100%;transform:translateY(100%)}.modal.max{top:0;left:0;right:auto;bottom:auto;width:100%;height:100%;max-width:100%;max-height:100%;transform:translateY(64rem);border-radius:0}.modal.left.small,.modal.right.small{width:320rem}.modal.left.medium,.modal.right.medium{width:512rem}.modal.left.large,.modal.right.large{width:704rem}.modal.top.small,.modal.bottom.small{height:256rem}.modal.top.medium,.modal.bottom.medium{height:384rem}.modal.top.large,.modal.bottom.large{height:512rem}nav>.modal,nav.left>.modal{z-index:0;text-align:left;overflow-y:auto;background-color:inherit;padding:16rem 16rem 16rem 80rem}nav.right>.modal{padding:16rem 80rem 16rem 16rem}nav.top>.modal{padding:80rem 48rem 16rem}nav.bottom>.modal{padding:16rem 48rem 80rem}.modal>a.row:hover,.modal>a.row.active{background-color:var(--secondary-container)}.modal>.row{padding:12rem 8rem}nav,.row{display:flex;align-items:center;justify-content:flex-start;white-space:nowrap;border-radius:0;gap:16rem}*:not(.divider,.small-divider,.medium-divider,.large-divider)+nav:not(.left,.right,.top,.bottom),*:not(.divider,.small-divider,.medium-divider,.large-divider)+.row:not(a){margin-top:16rem}nav *,.row *{margin-top:0;margin-bottom:0}nav>*,.row>*{margin:0!important;white-space:normal;flex:none}nav.no-space,.row.no-space{gap:0rem}nav.no-space>.border+.border,.row.no-space>.border+.border{border-left:0}nav.medium-space,.row.medium-space{gap:24rem}nav.large-space,.row.large-space{gap:32rem}nav>.max,.row>.max{flex:auto}nav.wrap,.row.wrap{display:flex;flex-wrap:wrap}header>nav,header>.row{min-height:64rem}footer>nav,footer>.row{min-height:80rem}nav>.border.no-margin+.border.no-margin,.row>.border.no-margin+.border.no-margin{border-left:0}nav.left,nav.right,nav.top,nav.bottom{display:flex;align-items:center;justify-content:center;flex-direction:column;border:0;position:fixed;color:var(--on-surface);background-color:var(--surface);transform:none;z-index:100;left:0;top:0;bottom:0;right:0;height:auto;width:auto;text-align:center;padding:8rem}nav.left,nav.right{width:80rem}nav.top,nav.bottom{height:80rem}nav.top{bottom:auto;justify-content:center;flex-direction:row}nav.left{right:auto;justify-content:flex-start;flex-direction:column}nav.right{left:auto;justify-content:flex-start;flex-direction:column}nav.bottom{top:auto;justify-content:center;flex-direction:row}nav.left>a:not(button,.button,.chip,img,video),nav.right>a:not(button,.button,.chip,img,video),nav.top>a:not(button,.button,.chip,img,video),nav.bottom>a:not(button,.button,.chip,img,video){min-width:56rem;min-height:56rem;text-align:center;display:flex;z-index:101;flex-direction:column}nav.left>a:not(button,.button,.chip,img,video),nav.right>a:not(button,.button,.chip,img,video){width:auto}nav.top>a:not(button,.button,.chip,img,video),nav.bottom>a:not(button,.button,.chip,img,video){height:auto;width:56rem}nav.left:before,nav.right:before,nav.top:before,nav.bottom:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background-color:inherit;z-index:101;border-radius:inherit}nav.left>:not(.modal,.overlay),nav.right>:not(.modal,.overlay),nav.top>:not(.modal,.overlay),nav.bottom>:not(.modal,.overlay){z-index:101}nav.left>a:not(button,.button,.chip)>i,nav.right>a:not(button,.button,.chip)>i,nav.top>a:not(button,.button,.chip)>i,nav.bottom>a:not(button,.button,.chip)>i{padding:4rem;border-radius:32rem;transition:var(--speed1) padding linear;margin:0 auto}nav.left>a:not(button,.button,.chip):hover>i,nav.left>a:not(button,.button,.chip):focus>i,nav.left>a:not(button,.button,.chip).active>i,nav.right>a:not(button,.button,.chip):hover>i,nav.right>a:not(button,.button,.chip):focus>i,nav.right>a:not(button,.button,.chip).active>i,nav.top>a:not(button,.button,.chip):hover>i,nav.top>a:not(button,.button,.chip):focus>i,nav.top>a:not(button,.button,.chip).active>i,nav.bottom>a:not(button,.button,.chip):focus>i,nav.bottom>a:not(button,.button,.chip):hover>i,nav.bottom>a:not(button,.button,.chip).active>i{background-color:var(--secondary-container);color:var(--on-secondary-container);padding:4rem 16rem;font-variation-settings:"FILL" 1}nav.left>.modal{padding-left:88rem}nav.right>.modal{padding-right:88rem}nav.top>.modal{padding-top:88rem}nav.bottom>.modal{padding-bottom:88rem}nav.left-align,nav.top-align{justify-content:flex-start}nav.right-align,nav.bottom-align{justify-content:flex-end}nav.center-align,nav.middle-align{justify-content:center}nav:not(.left,.right)>.space{width:8rem}nav:not(.left,.right)>.medium-space{width:16rem}nav:not(.left,.right)>.large-space{width:24rem}@media only screen and (max-width: 600px){nav.top,nav.bottom{justify-content:space-around}}.overlay{opacity:0;visibility:hidden;position:fixed;top:0;left:0;width:100%;height:100%;color:var(--on-background);background-color:var(--overlay);z-index:100;transition:var(--speed3) all,0s background-color}nav>.overlay{z-index:0}.overlay.active{opacity:1;visibility:visible}.page,.modal:not(.active) .page.active,.page:not(.active) .page.active{opacity:0;position:absolute;display:none}.page.active{opacity:1;position:inherit;display:block}.page.top.active{animation:var(--speed4) page-to-bottom ease}.page.bottom.active{animation:var(--speed4) page-to-top ease}.page.left.active{animation:var(--speed4) page-to-right ease}.page.right.active{animation:var(--speed4) page-to-left ease}@keyframes page-to-bottom{0%{opacity:0;transform:translateY(-64rem)}to{opacity:1;transform:translateY(0)}}@keyframes page-to-top{0%{opacity:0;transform:translateY(64rem)}to{opacity:1;transform:translateY(0)}}@keyframes page-to-left{0%{opacity:0;transform:translate(64rem)}to{opacity:1;transform:translate(0)}}@keyframes page-to-right{0%{opacity:0;transform:translate(-64rem)}to{opacity:1;transform:translate(0)}}.progress{position:absolute;background-color:var(--active);top:0;bottom:0;left:0;right:0;transition:var(--speed4) clip-path;clip-path:polygon(0% 0%,0% 0%,0% 0%,0% 0%)}.progress.left{clip-path:polygon(0% 0%,0% 100%,0% 100%,0% 0%)}.progress.right{clip-path:polygon(100% 0%,100% 100%,100% 100%,100% 0%)}.progress.top{clip-path:polygon(0% 0%,100% 0%,100% 0%,0% 0%)}.progress.bottom{clip-path:polygon(0% 100%,100% 100%,100% 100%,0% 100%)}.progress+*{margin-top:0}.checkbox,.radio,.switch{width:auto;height:auto;line-height:normal;white-space:nowrap;cursor:pointer;display:inline-flex;align-items:center}.checkbox>input,.radio>input{width:24rem;height:24rem;opacity:0}.checkbox>span,.radio>span,.switch>span{display:inline-flex;align-items:center;color:var(--on-background);font-size:14rem}.checkbox>span:not(:empty),.radio>span:not(:empty){padding-left:4rem}.checkbox>input+span:before,.radio>input+span:before,.switch>input+span:empty:after,.switch>input+span>i{font-family:Material Symbols Outlined;font-weight:400;font-style:normal;font-size:24rem;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:"liga";-webkit-font-smoothing:antialiased;vertical-align:middle;text-align:center;overflow:hidden;width:24rem;height:24rem;box-sizing:border-box;margin:0 auto;outline:none;color:var(--primary);position:absolute;left:-24rem;background-color:transparent;border-radius:50%}.checkbox>input+span:before{content:"check_box_outline_blank"}.radio>input+span:before{content:"radio_button_unchecked"}.checkbox>input:not([disabled]):focus+span:before,.checkbox>input:not([disabled]):hover+span:before,.radio>input:not([disabled]):focus+span:before,.radio>input:not([disabled]):hover+span:before{background-color:var(--active);box-shadow:0 0 0 8rem var(--active);animation:var(--speed1) to-checked ease-out}.checkbox>input:checked+span:before{color:var(--primary);content:"check_box";font-variation-settings:"FILL" 1}.radio>input:checked+span:before{color:var(--primary);content:"radio_button_checked"}.checkbox+.checkbox,.radio+.radio,.switch+.switch{margin-left:8rem}.switch>input{width:52rem;height:32rem;opacity:0}.switch>input+span:before{content:"";position:absolute;left:0;top:50%;background-color:var(--active);border:2rem solid var(--outline);box-sizing:border-box;width:52rem;height:32rem;border-radius:32rem;transform:translate(-52rem,-50%)}.switch>input:checked+span:before{border:none;background-color:var(--primary)}.switch>input+span:empty:after,.switch>input+span>i{position:absolute;left:0;top:50%;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;transition:var(--speed2) all;font-size:16rem;user-select:none;min-width:auto;content:"";color:var(--surface-variant);background-color:var(--outline);transform:translate(-48rem,-50%) scale(.6)}.switch>input:checked+span:empty:after,.switch>input:checked+span>i{content:"check";color:var(--primary);background-color:var(--on-primary);transform:translate(-28rem,-50%) scale(1)}.switch>input+span>i{transform:translate(-48rem,-50%) scale(1)}.switch>input+span>i:last-child,.switch>input:checked+span>i:first-child{opacity:0}.switch>input:checked+span>i:last-child,.switch>input+span>i:first-child{opacity:1}.switch>input:not([disabled]):focus+span:empty:after,.switch>input:not([disabled]):hover+span:empty:after,.switch>input:not([disabled]):focus+span>i,.switch>input:not([disabled]):hover+span>i{box-shadow:0 0 0 8rem var(--active)}.checkbox>input:disabled+span,.radio>input:disabled+span,.switch>input:disabled+span{opacity:.5;cursor:not-allowed}.field>nav,.field>.row{flex-grow:1;padding:0 16rem}.field.round>nav,.field.round>.row{flex-grow:1;padding:0 24rem}@keyframes to-checked{0%{box-shadow:0 0 0 0 var(--active)}to{box-shadow:0 0 0 8rem var(--active)}}table{width:100%;border-spacing:0;font-size:14rem;color:var(--on-background);text-align:left;border-radius:0}table td,table th{width:1%;text-align:inherit;padding:8rem}table th{font-weight:500}table.border td,table.border th{border-bottom:1rem solid var(--outline)}table.no-space td,table.no-space th{padding:0}table.medium-space td,table.medium-space th{padding:12rem}table.large-space td,table.large-space th{padding:16rem}td>.button,td>nav>.button,td>button,td>nav>button,td>.none,td>nav>.none,td>.chip,td>nav>.chip{min-height:24rem;max-height:24rem}td>.circle:not(.tiny,.small,.medium,.large,.extra),td>nav>.circle:not(.tiny,.small,.medium,.large,.extra),td>.square:not(.tiny,.small,.medium,.large,.extra),td>nav>.square:not(.tiny,.small,.medium,.large,.extra){min-width:24rem;max-width:24rem;min-height:24rem;max-height:24rem}.tabs{display:flex;white-space:nowrap;border-bottom:1rem solid var(--outline);border-radius:0}.tabs:not(.left-align,.right-align,.center-align){justify-content:space-around}*+.tabs{margin-top:16rem}.tabs>a{display:flex;font-size:14rem;font-weight:500;color:var(--on-background);padding:8rem 16rem;border-bottom:2rem solid transparent;text-align:center;min-height:48rem;width:100%;gap:4rem}.tabs.small>a{min-height:32rem}.tabs.medium>a{min-height:40rem}.tabs.large>a{min-height:64rem}.tabs>a.active{color:var(--primary);border-bottom:2rem solid var(--primary)}.tabs>a.active>i{color:var(--primary)}.tabs.left-align>a,.tabs.center-align>a,.tabs.right-align>a{width:auto}.toast{position:fixed;top:auto;bottom:96rem;left:50%;right:auto;width:80%;height:auto;z-index:200;visibility:hidden;display:flex;box-shadow:var(--elevate2);color:var(--on-error);background-color:var(--error-background);padding:16rem;opacity:1;cursor:pointer;transform:translate(-50%);text-align:left;align-items:center;border-radius:4rem;gap:8rem}.toast.top{top:96rem;bottom:auto}.toast.bottom{top:auto;bottom:72rem}.toast.active{visibility:visible;animation:var(--speed2) toast-to-top}.toast.active.top{animation:var(--speed2) toast-to-bottom}@keyframes toast-to-top{0%{opacity:0;transform:translate(-50%,16rem)}to{opacity:1;transform:translate(-50%)}}@keyframes toast-to-bottom{0%{opacity:0;transform:translate(-50%,-16rem)}to{opacity:1;transform:translate(-50%)}}@media only screen and (min-width: 993px){.toast{width:40%}}.tooltip{display:none;background-color:#000000e6;color:#fff;font-size:12rem;text-align:center;border-radius:4rem;padding:8rem;position:absolute;z-index:3;top:0;left:50%;bottom:auto;right:auto;transform:translate(-50%,-100%);width:auto;white-space:nowrap;font-weight:500}.tooltip.left{left:0;top:50%;bottom:auto;right:auto;transform:translate(-100%,-50%)}.tooltip.right{right:0;top:50%;bottom:auto;left:auto;transform:translate(100%,-50%)}.tooltip.top{top:0;left:50%;bottom:auto;right:auto;transform:translate(-50%,-100%)}.tooltip.bottom{bottom:0;left:50%;top:auto;right:auto;transform:translate(-50%,100%)}.tooltip.small{width:128rem;white-space:normal}.tooltip.medium{width:192rem;white-space:normal}.tooltip.large{width:256rem;white-space:normal}*:hover>.tooltip{display:flex;align-items:center;gap:8rem}.dropdown:active~.tooltip,button:focus>.dropdown~.tooltip,.button:focus>.dropdown~.tooltip,.field>:focus~.dropdown~.tooltip{display:none} diff --git a/web-client/iron-svelte-client/static/beercss/beer.min.js b/web-client/iron-svelte-client/static/beercss/beer.min.js new file mode 100644 index 00000000..2f6f0644 --- /dev/null +++ b/web-client/iron-svelte-client/static/beercss/beer.min.js @@ -0,0 +1 @@ +(()=>{if(window.ui)return;let g=null,x=null,k=null,m={light:"",dark:""};const Y=r=>new Promise(e=>setTimeout(e,r)),D=()=>"fxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,r=>{let e=Math.random()*16|0;return(r=="x"?e:e&3|8).toString(16)}),u=(r,e)=>{try{return typeof r=="string"?(e||document).querySelector(r):r}catch{}},c=(r,e)=>{try{return typeof r=="string"?(e||document).querySelectorAll(r):r}catch{}},a=(r,e)=>r?r.classList.contains(e):!1,p=(r,e)=>{!r||r.classList.add(e)},d=(r,e)=>{!r||r.classList.remove(e)},s=(r,e,t)=>{r.addEventListener(e,t,!0)},B=(r,e,t)=>{r.removeEventListener(e,t,!0)},T=(r,e)=>{if(!!e)return e.parentNode.insertBefore(r,e)},F=r=>{if(!!r)return r.previousElementSibling},j=r=>{if(!!r)return r.nextElementSibling},f=r=>{if(!!r)return r.parentElement},L=r=>{let e=document.createElement("div");for(let t in r)e[t]=r[t];return e},v=r=>{let e=r,t=f(r),o=u("label",t),i=a(t,"border")&&!a(t,"fill");if(document.activeElement==r||e.value||/date|time/.test(e.type)){if(i&&o){let n=a(o,"active")?o.offsetWidth:Math.round(o.offsetWidth/1.33),b=a(t,"round")?20:12,C=n+b+8;e.style.clipPath=`polygon(0% 0%, ${b}rem 0%, ${b}rem 8rem, ${C}rem 8rem, ${C}rem 0%, 100% 0%, 100% 100%, 0% 100%)`}else e.style.clipPath="";p(o,"active")}else d(o,"active"),e.style.clipPath="";r.getAttribute("data-ui")&&z(r)},q=r=>{let e=r.currentTarget;/input/i.test(e.tagName)||z(e)},A=r=>{let e=r.currentTarget,t=u("input:not([type=file]):not([type=checkbox]):not([type=radio]), select, textarea",f(e));t&&t.focus()},S=r=>{let e=r.currentTarget;v(e)},$=r=>{let e=r.currentTarget;v(e)},E=r=>{let e=r.currentTarget;c(".dropdown.active").forEach(o=>d(o,"active")),B(e,"click",E)},_=r=>{let e=r.currentTarget;d(e,"active"),g&&clearTimeout(g)},N=r=>{let e=r.currentTarget;w(e)},M=r=>{let e=r.currentTarget;w(e,r)},P=()=>{x&&clearTimeout(x),x=setTimeout(y,180)},w=(r,e)=>{if(e){if(e.key!=="Enter")return;let i=e.currentTarget,l=j(i);return!l||!/file/i.test(l.type)?void 0:l.click()}let t=r,o=F(r);!o||!/text/i.test(o.type)||(o.value=Array.from(t.files).map(i=>i.name).join(", "),o.readOnly=!0,o.addEventListener("keydown",M),v(o))},z=(r,e,t)=>{if(e||(e=u(r.getAttribute("data-ui"))),a(e,"modal"))return H(r,e);if(a(e,"dropdown"))return O(r,e);if(a(e,"toast"))return U(r,e,t);if(a(e,"page"))return I(r,e);if(a(e,"progress"))return R(e,t);if(h(r),a(e,"active"))return d(e,"active");p(e,"active")},h=r=>{let e=f(r);if(!a(e,"tabs"))return;c("a",e).forEach(o=>d(o,"active")),p(r,"active")},I=(r,e)=>{h(r);let t=f(e);for(let o=0;o{if(h(r),a(e,"active"))return d(e,"active");c(".dropdown.active").forEach(o=>d(o,"active")),p(e,"active"),s(document.body,"click",E)},H=async(r,e)=>{h(r);let t=F(e);a(t,"overlay")||(t=L({className:"overlay"}),T(t,e),await Y(90)),t.onclick=()=>{d(r,"active"),d(e,"active"),d(t,"active")};let o=a(e,"active"),i=f(e);/nav/i.test(i.tagName)&&c(".modal, a, .overlay",i).forEach(n=>d(n,"active")),o?(d(r,"active"),d(t,"active"),d(e,"active")):(!/button/i.test(r.tagName)&&!a(r,"button")&&!a(r,"chip")&&p(r,"active"),p(t,"active"),p(e,"active"))},U=(r,e,t)=>{h(r),c(".toast.active").forEach(i=>d(i,"active")),p(e,"active"),s(e,"click",_),g&&clearTimeout(g),!(t&&t==-1)&&(g=setTimeout(()=>{d(e,"active")},t&&t?t:6e3))},R=(r,e)=>{let t=r;if(a(t,"left"))return t.style.clipPath=`polygon(0% 0%, 0% 100%, ${e}% 100%, ${e}% 0%)`;if(a(t,"top"))return t.style.clipPath=`polygon(0% 0%, 100% 0%, 100% ${e}%, 0% ${e}%)`;if(a(t,"right"))return t.style.clipPath=`polygon(100% 0%, 100% 100%, ${100-e}% 100%, ${100-e}% 0%)`;if(a(t,"bottom"))return t.style.clipPath=`polygon(0% 100%, 100% 100%, 100% ${100-e}%, 0% ${100-e}%)`},V=()=>{if(m.light&&m.dark)return m;let r=document.createElement("body");r.className="light",document.body.appendChild(r);let e=document.createElement("body");e.className="dark",document.body.appendChild(e);let t=getComputedStyle(r),o=getComputedStyle(e),i=["--primary","--on-primary","--primary-container","--on-primary-container","--secondary","--on-secondary","--secondary-container","--on-secondary-container","--tertiary","--on-tertiary","--tertiary-container","--on-tertiary-container","--error","--on-error","--error-container","--on-error-container","--background","--on-background","--surface","--on-surface","--outline","--surface-variant","--on-surface-variant","--inverse-surface","--inverse-on-surface"];for(let l=0;l{if(!r||!window.materialDynamicColors)return V();let e=/dark/i.test(document.body.className)?"dark":"light";return r&&r.light&&r.dark?(m.light=r.light,m.dark=r.dark,document.body.setAttribute("style",r[e]),r):window.materialDynamicColors(r).then(t=>{const o=i=>{let l="";for(let n in i){let b=n.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g,"$1-$2").toLowerCase();l+="--"+b+":"+i[n]+";"}return l};return m.light=o(t.light),m.dark=o(t.dark),document.body.setAttribute("style",m[e]),m})},Z=r=>r?(document.body.classList.remove("light","dark"),document.body.classList.add(r),window.materialDynamicColors&&document.body.setAttribute("style",m[r]),r):/dark/i.test(document.body.className)?"dark":"light",K=()=>{k||(k=new MutationObserver(P),k.observe(document.body,{childList:!0,subtree:!0}),y())},y=(r,e)=>{if(r){if(r=="setup")return K();if(r=="guid")return D();if(r=="mode")return Z(e);if(r=="theme")return W(e);let n=u(r),b=u("[data-ui='#"+n.id+"']");z(b,n,e)}c("[data-ui]").forEach(n=>s(n,"click",q)),c(".field > label").forEach(n=>s(n,"click",A)),c(".field > input:not([type=file]):not([type=checkbox]):not([type=radio]), .field > select, .field > textarea").forEach(n=>{s(n,"focus",S),s(n,"blur",$),v(n)}),c(".field > input[type=file]").forEach(n=>{s(n,"change",N),w(n)})};window.ui=y,window.addEventListener("load",()=>y("setup"))})(); diff --git a/web-client/iron-svelte-client/static/crosshair.png b/web-client/iron-svelte-client/static/crosshair.png new file mode 100644 index 00000000..a3be0181 Binary files /dev/null and b/web-client/iron-svelte-client/static/crosshair.png differ diff --git a/web-client/iron-svelte-client/static/favicon.png b/web-client/iron-svelte-client/static/favicon.png new file mode 100644 index 00000000..d82ea274 Binary files /dev/null and b/web-client/iron-svelte-client/static/favicon.png differ diff --git a/web-client/iron-svelte-client/static/material-icons/LICENSE b/web-client/iron-svelte-client/static/material-icons/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/web-client/iron-svelte-client/static/material-icons/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/web-client/iron-svelte-client/static/material-icons/filled.css b/web-client/iron-svelte-client/static/material-icons/filled.css new file mode 100644 index 00000000..5492184a --- /dev/null +++ b/web-client/iron-svelte-client/static/material-icons/filled.css @@ -0,0 +1,24 @@ +@font-face { + font-family: "Material Icons"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons.woff2") format("woff2"); +} +.material-icons { + font-family: "Material Icons"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} diff --git a/web-client/iron-svelte-client/static/material-icons/index.css b/web-client/iron-svelte-client/static/material-icons/index.css new file mode 100644 index 00000000..676c4a29 --- /dev/null +++ b/web-client/iron-svelte-client/static/material-icons/index.css @@ -0,0 +1,124 @@ +@font-face { + font-family: "Material Icons"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons.woff2") format("woff2"); +} +.material-icons { + font-family: "Material Icons"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} + +@font-face { + font-family: "Material Icons Outlined"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons-outlined.woff2") format("woff2"); +} +.material-icons-outlined { + font-family: "Material Icons Outlined"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} + +@font-face { + font-family: "Material Icons Round"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons-round.woff2") format("woff2"); +} +.material-icons-round { + font-family: "Material Icons Round"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} + +@font-face { + font-family: "Material Icons Sharp"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons-sharp.woff2") format("woff2"); +} +.material-icons-sharp { + font-family: "Material Icons Sharp"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} + +@font-face { + font-family: "Material Icons Two Tone"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons-two-tone.woff2") format("woff2"); +} +.material-icons-two-tone { + font-family: "Material Icons Two Tone"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} diff --git a/web-client/iron-svelte-client/static/material-icons/material-icons-outlined.woff2 b/web-client/iron-svelte-client/static/material-icons/material-icons-outlined.woff2 new file mode 100644 index 00000000..d44b9486 Binary files /dev/null and b/web-client/iron-svelte-client/static/material-icons/material-icons-outlined.woff2 differ diff --git a/web-client/iron-svelte-client/static/material-icons/material-icons-round.woff2 b/web-client/iron-svelte-client/static/material-icons/material-icons-round.woff2 new file mode 100644 index 00000000..e9e305f2 Binary files /dev/null and b/web-client/iron-svelte-client/static/material-icons/material-icons-round.woff2 differ diff --git a/web-client/iron-svelte-client/static/material-icons/material-icons-sharp.woff2 b/web-client/iron-svelte-client/static/material-icons/material-icons-sharp.woff2 new file mode 100644 index 00000000..40626852 Binary files /dev/null and b/web-client/iron-svelte-client/static/material-icons/material-icons-sharp.woff2 differ diff --git a/web-client/iron-svelte-client/static/material-icons/material-icons-two-tone.woff2 b/web-client/iron-svelte-client/static/material-icons/material-icons-two-tone.woff2 new file mode 100644 index 00000000..8f799901 Binary files /dev/null and b/web-client/iron-svelte-client/static/material-icons/material-icons-two-tone.woff2 differ diff --git a/web-client/iron-svelte-client/static/material-icons/material-icons.woff2 b/web-client/iron-svelte-client/static/material-icons/material-icons.woff2 new file mode 100644 index 00000000..5492a6e7 Binary files /dev/null and b/web-client/iron-svelte-client/static/material-icons/material-icons.woff2 differ diff --git a/web-client/iron-svelte-client/static/material-icons/outlined.css b/web-client/iron-svelte-client/static/material-icons/outlined.css new file mode 100644 index 00000000..4d707fd9 --- /dev/null +++ b/web-client/iron-svelte-client/static/material-icons/outlined.css @@ -0,0 +1,24 @@ +@font-face { + font-family: "Material Icons Outlined"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons-outlined.woff2") format("woff2"); +} +.material-icons-outlined { + font-family: "Material Icons Outlined"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} diff --git a/web-client/iron-svelte-client/static/material-icons/round.css b/web-client/iron-svelte-client/static/material-icons/round.css new file mode 100644 index 00000000..b179a476 --- /dev/null +++ b/web-client/iron-svelte-client/static/material-icons/round.css @@ -0,0 +1,24 @@ +@font-face { + font-family: "Material Icons Round"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons-round.woff2") format("woff2"); +} +.material-icons-round { + font-family: "Material Icons Round"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} diff --git a/web-client/iron-svelte-client/static/material-icons/sharp.css b/web-client/iron-svelte-client/static/material-icons/sharp.css new file mode 100644 index 00000000..9334db76 --- /dev/null +++ b/web-client/iron-svelte-client/static/material-icons/sharp.css @@ -0,0 +1,24 @@ +@font-face { + font-family: "Material Icons Sharp"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons-sharp.woff2") format("woff2"); +} +.material-icons-sharp { + font-family: "Material Icons Sharp"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} diff --git a/web-client/iron-svelte-client/static/material-icons/two-tone.css b/web-client/iron-svelte-client/static/material-icons/two-tone.css new file mode 100644 index 00000000..6b97bef0 --- /dev/null +++ b/web-client/iron-svelte-client/static/material-icons/two-tone.css @@ -0,0 +1,24 @@ +@font-face { + font-family: "Material Icons Two Tone"; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("./material-icons-two-tone.woff2") format("woff2"); +} +.material-icons-two-tone { + font-family: "Material Icons Two Tone"; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: "liga"; +} diff --git a/web-client/iron-svelte-client/static/theme.css b/web-client/iron-svelte-client/static/theme.css new file mode 100644 index 00000000..affb877f --- /dev/null +++ b/web-client/iron-svelte-client/static/theme.css @@ -0,0 +1,59 @@ +body.light { + --primary: #0061a6; + --on-primary: #ffffff; + --primary-container: #d0e4ff; + --on-primary-container: #001d36; + --secondary: #535f70; + --on-secondary: #ffffff; + --secondary-container: #d6e3f7; + --on-secondary-container: #101c2b; + --tertiary: #6b5778; + --on-tertiary: #ffffff; + --tertiary-container: #f3daff; + --on-tertiary-container: #251432; + --error: #ba1b1b; + --error-container: #ffdad4; + --on-error: #ffffff; + --on-error-container: #410001; + --background: #fdfcff; + --on-background: #1b1b1b; + --surface: #fdfcff; + --on-surface: #1b1b1b; + --surface-variant: #dfe2eb; + --on-surface-variant: #42474e; + --outline: #73777f; + --inverse-on-surface: #f1f0f4; + --inverse-surface: #2f3033; + --inverse-primary: #9ccaff; + --shadow: #000000; +} + +body.dark { + --primary: #9ccaff; + --on-primary: #00325a; + --primary-container: #00497f; + --on-primary-container: #d0e4ff; + --secondary: #bbc8db; + --on-secondary: #253140; + --secondary-container: #3c4858; + --on-secondary-container: #d6e3f7; + --tertiary: #d6bee4; + --on-tertiary: #3b2948; + --tertiary-container: #523f5f; + --on-tertiary-container: #f3daff; + --error: #ffb4a9; + --error-container: #930006; + --on-error: #680003; + --on-error-container: #ffdad4; + --background: #1b1b1b; + --on-background: #e2e2e6; + --surface: #1b1b1b; + --on-surface: #e2e2e6; + --surface-variant: #42474e; + --on-surface-variant: #c3c7d0; + --outline: #8d9199; + --inverse-on-surface: #1b1b1b; + --inverse-surface: #e2e2e6; + --inverse-primary: #0061a6; + --shadow: #000000; +} diff --git a/web-client/iron-svelte-client/svelte.config.js b/web-client/iron-svelte-client/svelte.config.js new file mode 100644 index 00000000..2fa36334 --- /dev/null +++ b/web-client/iron-svelte-client/svelte.config.js @@ -0,0 +1,21 @@ +import adapter from '@sveltejs/adapter-static'; +import preprocess from 'svelte-preprocess'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://github.com/sveltejs/svelte-preprocess + // for more information about preprocessors + preprocess: preprocess(), + + kit: { + adapter: adapter({ + pages: 'build/browser', + assets: 'build/browser', + fallback: null, + precompress: false, + strict: true, + }), + }, +}; + +export default config; diff --git a/web-client/iron-svelte-client/tsconfig.json b/web-client/iron-svelte-client/tsconfig.json new file mode 100644 index 00000000..55f97d44 --- /dev/null +++ b/web-client/iron-svelte-client/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "noImplicitAny": true + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/web-client/iron-svelte-client/vite.config.ts b/web-client/iron-svelte-client/vite.config.ts new file mode 100644 index 00000000..011b7469 --- /dev/null +++ b/web-client/iron-svelte-client/vite.config.ts @@ -0,0 +1,16 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import type { UserConfig } from 'vite'; +import wasm from 'vite-plugin-wasm'; +import topLevelAwait from 'vite-plugin-top-level-await'; + +const config: UserConfig = { + mode: 'process.env.MODE' || 'development', + plugins: [sveltekit(), wasm(), topLevelAwait()], + server: { + fs: { + allow: ['./static'], + }, + }, +}; + +export default config; diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..d3c7fa3a --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "xtask" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "xtask" +test = false + +[dependencies] +anyhow = "1" +pico-args = "0.5" +xshell = "0.2" +tinyjson = "2.5" + +[lints] +workspace = true + diff --git a/xtask/README.md b/xtask/README.md new file mode 100644 index 00000000..7e8c35a7 --- /dev/null +++ b/xtask/README.md @@ -0,0 +1,3 @@ +# IronRDP project automation + +Free-form automation following [`cargo xtask`](https://github.com/matklad/cargo-xtask) specification. diff --git a/xtask/src/bin_install.rs b/xtask/src/bin_install.rs new file mode 100644 index 00000000..7c7aed3e --- /dev/null +++ b/xtask/src/bin_install.rs @@ -0,0 +1,88 @@ +use xshell::{cmd, Shell}; + +use crate::macros::trace; +use crate::{local_bin, CARGO, LOCAL_CARGO_ROOT}; + +pub struct CargoPackage { + pub name: &'static str, + pub binary_name: &'static str, + pub version: &'static str, +} + +impl CargoPackage { + pub const fn new(name: &'static str, version: &'static str) -> Self { + Self { + name, + binary_name: name, + version, + } + } + + pub const fn with_binary_name(self, name: &'static str) -> Self { + Self { + binary_name: name, + ..self + } + } +} + +pub fn cargo_install(sh: &Shell, package: &CargoPackage) -> anyhow::Result<()> { + let package_name = package.name; + let package_version = package.version; + + if is_installed(sh, package) { + trace!("{package_name} is already installed"); + return Ok(()); + } + + if cargo_binstall_is_available(sh) { + trace!("cargo-binstall is available"); + cmd!( + sh, + "{CARGO} binstall --no-confirm --root {LOCAL_CARGO_ROOT} {package_name}@{package_version}" + ) + .run()?; + } else { + trace!("Install {package_name} using cargo install"); + // Install in debug because it's faster to compile and we typically don't need execution speed anyway. + cmd!( + sh, + "{CARGO} install --debug --locked --root {LOCAL_CARGO_ROOT} {package_name}@{package_version}" + ) + .run()?; + } + + Ok(()) +} + +fn cargo_binstall_is_available(sh: &Shell) -> bool { + cmd!(sh, "{CARGO} binstall -h") + .quiet() + .ignore_stderr() + .ignore_stdout() + .run() + .is_ok() +} + +/// Checks if a binary is installed in local root +pub fn is_installed(sh: &Shell, package: impl GetBinaryName) -> bool { + let path = local_bin().join(package.binary_name()); + sh.path_exists(&path) || sh.path_exists(path.with_extension("exe")) +} + +#[doc(hidden)] +pub trait GetBinaryName { + fn binary_name(self) -> &'static str; +} + +impl GetBinaryName for &CargoPackage { + fn binary_name(self) -> &'static str { + self.binary_name + } +} + +impl GetBinaryName for &'static str { + fn binary_name(self) -> &'static str { + self + } +} diff --git a/xtask/src/bin_version.rs b/xtask/src/bin_version.rs new file mode 100644 index 00000000..2a179453 --- /dev/null +++ b/xtask/src/bin_version.rs @@ -0,0 +1,14 @@ +// We pin the binaries to specific versions so we use the same artifact everywhere. +// Hash of this file is used in CI for caching. + +use crate::bin_install::CargoPackage; + +pub const CARGO_FUZZ: CargoPackage = CargoPackage::new("cargo-fuzz", "0.12.0"); +pub const CARGO_LLVM_COV: CargoPackage = CargoPackage::new("cargo-llvm-cov", "0.6.16"); +pub const GRCOV: CargoPackage = CargoPackage::new("grcov", "0.8.20"); +pub const WASM_PACK: CargoPackage = CargoPackage::new("wasm-pack", "0.13.1"); +pub const TYPOS_CLI: CargoPackage = CargoPackage::new("typos-cli", "1.29.5").with_binary_name("typos"); +pub const DIPLOMAT_TOOL: CargoPackage = CargoPackage::new("diplomat-tool", "0.7.1"); + +pub const WABT_VERSION: &str = "1.0.36"; +pub const NIGHTLY_TOOLCHAIN: &str = "nightly-2025-07-28"; diff --git a/xtask/src/check.rs b/xtask/src/check.rs new file mode 100644 index 00000000..ed44a721 --- /dev/null +++ b/xtask/src/check.rs @@ -0,0 +1,96 @@ +use crate::prelude::*; + +pub fn fmt(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("FORMATTING"); + + let output = cmd!(sh, "{CARGO} fmt --all -- --check").ignore_status().output()?; + + if !output.status.success() { + anyhow::bail!("Bad formatting, please run 'cargo +stable fmt --all'"); + } + + println!("All good!"); + + Ok(()) +} + +pub fn lints(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("LINTS"); + + // TODO: when 1.74 is released use `--keep-going`: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#keep-going + cmd!( + sh, + "{CARGO} clippy --workspace --all-targets --features helper,__bench --locked -- -D warnings" + ) + .run()?; + + println!("All good!"); + + Ok(()) +} + +pub fn typos(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("TYPOS-CLI"); + + if !is_installed(sh, "typos") { + anyhow::bail!("`typos-cli` binary is missing. Please run `cargo xtask check install`."); + } + + cmd!(sh, "typos").run()?; + + println!("All good!"); + Ok(()) +} + +pub fn install(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("TYPOS-CLI-INSTALL"); + + cargo_install(sh, &TYPOS_CLI)?; + + Ok(()) +} + +pub fn tests_compile(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("TESTS-COMPILE"); + cmd!(sh, "{CARGO} test --workspace --locked --no-run").run()?; + println!("All good!"); + Ok(()) +} + +pub fn tests_run(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("TESTS-RUN"); + cmd!(sh, "{CARGO} test --workspace --locked").run()?; + println!("All good!"); + Ok(()) +} + +pub fn lock_files(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("CHECK-LOCKS"); + + // Note that we can’t really use the --locked option of cargo, because to + // run xtask, we need to compile it using cargo first, and thus the lock + // files are already "refreshed" as far as cargo is concerned. Instead, + // this task will check for modifications to the lock files using git-status + // porcelain. The side benefit is that we can check for npm lock files too. + + const LOCK_FILES: &[&str] = &[ + "Cargo.lock", + "fuzz/Cargo.lock", + "web-client/iron-remote-desktop/package-lock.json", + "web-client/iron-remote-desktop-rdp/package-lock.json", + "web-client/iron-svelte-client/package-lock.json", + ]; + + let output = cmd!(sh, "git status --porcelain --untracked-files=no") + .args(LOCK_FILES) + .read()?; + + if !output.is_empty() { + cmd!(sh, "git status").run()?; + anyhow::bail!("one or more lock files are changed, you should commit those"); + } + + println!("All good!"); + + Ok(()) +} diff --git a/xtask/src/clean.rs b/xtask/src/clean.rs new file mode 100644 index 00000000..27fed338 --- /dev/null +++ b/xtask/src/clean.rs @@ -0,0 +1,25 @@ +use crate::prelude::*; + +pub fn workspace(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("CLEAN"); + + println!("Remove wasm package folder..."); + sh.remove_path("./crates/ironrdp-web/pkg")?; + println!("Done."); + + println!("Remove npm folders..."); + sh.remove_path("./web-client/iron-remote-desktop/node_modules")?; + sh.remove_path("./web-client/iron-remote-desktop/dist")?; + sh.remove_path("./web-client/iron-remote-desktop-rdp/node_modules")?; + sh.remove_path("./web-client/iron-remote-desktop-rdp/dist")?; + sh.remove_path("./web-client/iron-svelte-client/node_modules")?; + println!("Done."); + + println!("Remove local cargo root folder..."); + sh.remove_path("./.cargo/local_root")?; + println!("Done."); + + cmd!(sh, "{CARGO} clean").run()?; + + Ok(()) +} diff --git a/xtask/src/cli.rs b/xtask/src/cli.rs new file mode 100644 index 00000000..88b7eedf --- /dev/null +++ b/xtask/src/cli.rs @@ -0,0 +1,187 @@ +const HELP: &str = "\ +cargo xtask + +USAGE: + cargo xtask [OPTIONS] [TASK] + +FLAGS: + -h, --help Prints help information + -v, --verbose Prints additional execution traces + +TASKS: + bootstrap Install all requirements for development + check fmt Check formatting + check lints Check lints + check locks Check for dirty or staged lock files not yet committed + check tests [--no-run] Compile tests and, unless specified otherwise, run them + check typos Check for typos in the codebase + check install Install all requirements for check tasks + ci Run all checks required on CI + clean Clean workspace + cov grcov Generate a nice HTML report using code-coverage data from tests and fuzz targets + cov install Install cargo-llvm-cov in cargo local root + cov report-gh --repo --pr + Generate a coverage report, posting a comment in GitHub PR + cov report [--html] Generate a coverage report (optionally, a HTML report) + cov update Update coverage data in the cov-data branch + fuzz corpus-fetch Fetch fuzzing corpus from Azure storage + fuzz corpus-min [--target ] + Minify fuzzing corpus for a specific target (or all if unspecified) + fuzz corpus-push Push fuzzing corpus to Azure storage + fuzz install Install dependencies required for fuzzing + fuzz run [--duration ] [--target ] + Fuzz a specific target if any or all targets for a limited duration (default is 5s) + wasm check Ensure WASM module is compatible for the web + wasm install Install dependencies required to build the WASM target + web check Ensure Web Client is building without error + web install Install dependencies required to build and run Web Client + web build Build the Web Client + web run Run SvelteKit-based standalone Web Client + ffi install Install all requirements for ffi tasks + ffi build [--release] Build DLL for FFI (default is debug) + ffi bindings [--skip-dotnet-build] + Generate C# bindings for FFI, optionally skipping the .NET build +"; + +pub fn print_help() { + println!("{HELP}"); +} + +pub struct Args { + pub verbose: bool, + pub action: Action, +} + +pub enum Action { + ShowHelp, + Bootstrap, + CheckFmt, + CheckLints, + CheckLocks, + CheckTests { + no_run: bool, + }, + CheckTypos, + CheckInstall, + Ci, + Clean, + CovGrcov, + CovInstall, + CovReportGitHub { + repo: String, + pr: u32, + }, + CovReport { + html_report: bool, + }, + CovUpdate, + FuzzCorpusFetch, + FuzzCorpusMin { + target: Option, + }, + FuzzCorpusPush, + FuzzInstall, + FuzzRun { + duration: Option, + target: Option, + }, + WasmCheck, + WasmInstall, + WebCheck, + WebInstall, + WebBuild, + WebRun, + FfiInstall, + FfiBuildDll { + release: bool, + }, + FfiBuildBindings { + skip_dotnet_build: bool, + }, +} + +pub fn parse_args() -> anyhow::Result { + let mut args = pico_args::Arguments::from_env(); + + let action = if args.contains(["-h", "--help"]) { + Action::ShowHelp + } else { + match args.subcommand()?.as_deref() { + Some("bootstrap") => Action::Bootstrap, + Some("check") => match args.subcommand()?.as_deref() { + Some("fmt") => Action::CheckFmt, + Some("lints") => Action::CheckLints, + Some("locks") => Action::CheckLocks, + Some("tests") => Action::CheckTests { + no_run: args.contains("--no-run"), + }, + Some("typos") => Action::CheckTypos, + Some("install") => Action::CheckInstall, + Some(unknown) => anyhow::bail!("unknown check action: {unknown}"), + None => Action::ShowHelp, + }, + Some("ci") => Action::Ci, + Some("clean") => Action::Clean, + Some("cov") => match args.subcommand()?.as_deref() { + Some("grcov") => Action::CovGrcov, + Some("install") => Action::CovInstall, + Some("report-gh") => Action::CovReportGitHub { + repo: args.value_from_str("--repo")?, + pr: args.value_from_str("--pr")?, + }, + Some("report") => Action::CovReport { + html_report: args.contains("--html"), + }, + Some("update") => Action::CovUpdate, + None | Some(_) => anyhow::bail!("Unknown cov action"), + }, + Some("fuzz") => match args.subcommand()?.as_deref() { + Some("corpus-fetch") => Action::FuzzCorpusFetch, + Some("corpus-min") => Action::FuzzCorpusMin { + target: args.opt_value_from_str("--target")?, + }, + Some("corpus-push") => Action::FuzzCorpusPush, + Some("install") => Action::FuzzInstall, + Some("run") => Action::FuzzRun { + duration: args.opt_value_from_str("--duration")?, + target: args.opt_value_from_str("--target")?, + }, + None => Action::FuzzRun { + duration: None, + target: None, + }, + Some(unknown) => anyhow::bail!("unknown fuzz action: {unknown}"), + }, + Some("wasm") => match args.subcommand()?.as_deref() { + Some("check") => Action::WasmCheck, + Some("install") => Action::WasmInstall, + Some(unknown) => anyhow::bail!("unknown wasm action: {unknown}"), + None => Action::ShowHelp, + }, + Some("web") => match args.subcommand()?.as_deref() { + Some("check") => Action::WebCheck, + Some("install") => Action::WebInstall, + Some("build") => Action::WebBuild, + Some("run") => Action::WebRun, + Some(unknown) => anyhow::bail!("unknown web action: {unknown}"), + None => Action::ShowHelp, + }, + Some("ffi") => match args.subcommand()?.as_deref() { + Some("install") => Action::FfiInstall, + Some("build") => Action::FfiBuildDll { + release: args.contains("--release"), + }, + Some("bindings") => Action::FfiBuildBindings { + skip_dotnet_build: args.contains("--skip-dotnet-build"), + }, + Some(unknown) => anyhow::bail!("unknown ffi action: {unknown}"), + None => Action::ShowHelp, + }, + None | Some(_) => Action::ShowHelp, + } + }; + + let verbose = args.contains(["-v", "--verbose"]); + + Ok(Args { verbose, action }) +} diff --git a/xtask/src/cov.rs b/xtask/src/cov.rs new file mode 100644 index 00000000..50e073ab --- /dev/null +++ b/xtask/src/cov.rs @@ -0,0 +1,338 @@ +use core::fmt; + +use crate::prelude::*; + +const COV_IGNORE_REGEX: &str = + "(crates/ironrdp-(session|.+generators|.+glutin.+|replay|client|fuzzing|tokio|web|futures|tls)|xtask|testsuite)"; + +pub fn install(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("COV-INSTALL"); + + cargo_install(sh, &CARGO_LLVM_COV)?; + + Ok(()) +} + +pub fn update(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("COV-UPDATE"); + + let report = CoverageReport::generate(sh)?; + println!("New:\n{report}"); + + let initial_branch = cmd!(sh, "git rev-parse --abbrev-ref HEAD").read()?; + + println!("Switch branch"); + let _ = cmd!(sh, "git branch -D cov-data").run(); + cmd!(sh, "git checkout --orphan cov-data").run()?; + + let result = || -> anyhow::Result<()> { + cmd!(sh, "git rm --cached -r .").run()?; + + sh.write_file("./report.json", report.original_json_data)?; + + cmd!(sh, "git add ./report.json").run()?; + cmd!(sh, "git commit -m 'cov: update report data'").run()?; + cmd!(sh, "git push --force --set-upstream origin cov-data").run()?; + + Ok(()) + }(); + + println!("Clean working tree"); + cmd!(sh, "git clean -df").run()?; + + println!("Switch back to initial branch"); + cmd!(sh, "git checkout {initial_branch}").run()?; + + result?; + + Ok(()) +} + +pub fn report(sh: &Shell, html_report: bool) -> anyhow::Result<()> { + let _s = Section::new("COV-REPORT"); + + if html_report { + cmd!(sh, "{CARGO} llvm-cov --html") + .arg("--ignore-filename-regex") + .arg(COV_IGNORE_REGEX) + .run()?; + } else { + let report = CoverageReport::generate(sh)?; + let past_report = CoverageReport::past_report(sh)?; + + println!("Past:\n{past_report}"); + println!("New:\n{report}"); + println!( + "Diff: {:+.2}%", + report.covered_lines_percent - past_report.covered_lines_percent + ); + } + + Ok(()) +} + +pub fn report_github(sh: &Shell, repo: &str, pr_id: u32) -> anyhow::Result<()> { + use core::fmt::Write as _; + + const COMMENT_HEADER: &str = "## Coverage Report :robot: :gear:"; + const DIFF_THRESHOLD: f64 = 0.005; + + let _s = Section::new("COV-REPORT"); + + let report = CoverageReport::generate(sh)?; + let past_report = CoverageReport::past_report(sh)?; + + let diff = report.covered_lines_percent - past_report.covered_lines_percent; + + println!("Past:\n{past_report}"); + println!("New:\n{report}"); + println!("Diff: {diff:+}%"); + + // `GH_TOKEN` environment variable sanity checks + match std::env::var_os("GH_TOKEN") { + Some(value) if value.is_empty() => trace!("WARNING: `GH_TOKEN` environment variable is empty"), + Some(value) if value.is_ascii() => trace!("`GH_TOKEN` environment variable appears to be set properly"), + Some(_) => trace!("WARNING: `GH_TOKEN` environment variable's value is not an ASCII string"), + None => trace!("WARNING: `GH_TOKEN` environment variable is not set"), + } + + let comments = cmd!(sh, "gh api") + .arg("-H") + .arg("Accept: application/vnd.github.v3+json") + .arg(format!("/repos/{repo}/issues/{pr_id}/comments")) + .read()?; + + let comments: tinyjson::JsonValue = comments.parse().context("GitHub comments")?; + let comments = comments.get::>().context("comments list")?; + + let mut prev_comment_id = None; + + for comment in comments { + let body = comment["body"].get::().context("comment body")?; + + if body.starts_with(COMMENT_HEADER) { + let comment_id = get_json_int(comment, "id")?; + prev_comment_id = Some(comment_id); + break; + } + } + + let mut body = String::new(); + + writeln!(body, "{COMMENT_HEADER}")?; + writeln!(body, "**Past**:\n{past_report}")?; + writeln!(body, "**New**:\n{report}")?; + writeln!(body, "**Diff**: {diff:+.2}%")?; + writeln!(body, "\n[this comment will be updated automatically]")?; + + let command = cmd!(sh, "gh api") + .arg("-H") + .arg("Accept: application/vnd.github.v3+json") + .arg("-f") + .arg(format!("body={body}")); + + if let Some(comment_id) = prev_comment_id { + println!("Update existing comment"); + + command + .arg("--method") + .arg("PATCH") + .arg(format!("/repos/{repo}/issues/comments/{comment_id}")) + .ignore_stdout() + .run()?; + } else if diff.abs() > DIFF_THRESHOLD { + trace!("Diff ({diff}) is greater than threshold ({DIFF_THRESHOLD})"); + println!("Create new comment"); + + command + .arg("--method") + .arg("POST") + .arg(format!("/repos/{repo}/issues/{pr_id}/comments")) + .ignore_stdout() + .run()?; + } else { + println!("Coverage didn't change, skip GitHub comment"); + } + + Ok(()) +} + +pub fn grcov(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("COV-GRCOV"); + + cmd!(sh, "rustup install {NIGHTLY_TOOLCHAIN} --profile=minimal").run()?; + cmd!( + sh, + "rustup component add --toolchain {NIGHTLY_TOOLCHAIN} llvm-tools-preview" + ) + .run()?; + cmd!(sh, "rustup component add llvm-tools-preview").run()?; + + cargo_install(sh, &CARGO_FUZZ)?; + cargo_install(sh, &GRCOV)?; + + println!("Remove leftovers"); + sh.remove_path("./fuzz/coverage/")?; + sh.remove_path("./coverage/")?; + + sh.create_dir("./coverage/binaries")?; + + if cfg!(not(target_os = "windows")) { + // Fuzz coverage + + let _guard = sh.push_dir("./fuzz"); + + cmd!(sh, "{CARGO} clean").run()?; + + for target in FUZZ_TARGETS { + cmd!(sh, "rustup run {NIGHTLY_TOOLCHAIN} cargo fuzz coverage {target}").run()?; + } + + cmd!(sh, "cp -r ./target ../coverage/binaries/").run()?; + } + + { + // Test coverage + + cmd!(sh, "{CARGO} clean").run()?; + + cmd!(sh, "rustup run {NIGHTLY_TOOLCHAIN} cargo test --workspace") + .env("CARGO_INCREMENTAL", "0") + .env("RUSTFLAGS", "-C instrument-coverage") + .env("LLVM_PROFILE_FILE", "./coverage/default-%m-%p.profraw") + .run()?; + + cmd!(sh, "cp -r ./target/debug ./coverage/binaries/").run()?; + } + + sh.create_dir("./docs")?; + + cmd!( + sh, + "grcov . ./fuzz + --source-dir . + --binary-path ./coverage/binaries/ + --output-type html + --branch + --ignore-not-existing + --ignore xtask/* + --ignore src/* + --ignore **/tests/* + --ignore crates/*-generators/* + --ignore crates/web/* + --ignore crates/client/* + --ignore crates/glutin-renderer/* + --ignore crates/glutin-client/* + --ignore crates/replay-client/* + --ignore crates/tls/* + --ignore fuzz/fuzz_targets/* + --ignore target/* + --ignore fuzz/target/* + --excl-start begin-no-coverage + --excl-stop end-no-coverage + -o ./docs/coverage" + ) + .run()?; + + println!("Code coverage report available in `./docs/coverage` folder"); + + println!("Clean up"); + + sh.remove_path("./coverage/")?; + sh.remove_path("./fuzz/coverage/")?; + sh.remove_path("./xtask/coverage/")?; + + sh.read_dir("./crates")? + .into_iter() + .try_for_each(|crate_path| -> xshell::Result<()> { + for path in sh.read_dir(crate_path)? { + if path.ends_with("coverage") { + sh.remove_path(path)?; + } + } + Ok(()) + })?; + + Ok(()) +} + +struct CoverageReport { + total_lines: u64, + covered_lines: u64, + covered_lines_percent: f64, + original_json_data: String, +} + +impl CoverageReport { + fn from_json_value(lines: &tinyjson::JsonValue) -> anyhow::Result { + let total_lines = get_json_int(lines, "count")?; + let covered_lines = get_json_int(lines, "covered")?; + let covered_lines_percent = get_json_float(lines, "percent")?; + + let original_json_data = lines.stringify().context("original json data")?; + + Ok(Self { + total_lines, + covered_lines, + covered_lines_percent, + original_json_data, + }) + } + + fn generate(sh: &Shell) -> anyhow::Result { + let output = cmd!( + sh, + "{CARGO} llvm-cov + --ignore-filename-regex {COV_IGNORE_REGEX} + --json" + ) + .read()?; + + let report: tinyjson::JsonValue = output.parse().context("invalid JSON from cargo-llvm-cov")?; + + let lines = &report["data"][0]["totals"]["lines"]; + + Self::from_json_value(lines) + } + + fn past_report(sh: &Shell) -> anyhow::Result { + cmd!(sh, "git fetch origin cov-data").run()?; + + let output = cmd!(sh, "git show origin/cov-data:report.json").read()?; + + let lines: tinyjson::JsonValue = output + .parse() + .context("invalid JSON from origin/cov-data:report.json")?; + + Self::from_json_value(&lines) + } +} + +impl fmt::Display for CoverageReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Total lines: {}", self.total_lines)?; + writeln!( + f, + "Covered lines: {} ({:.2}%)", + self.covered_lines, self.covered_lines_percent + )?; + Ok(()) + } +} + +fn get_json_float(value: &tinyjson::JsonValue, key: &str) -> anyhow::Result { + value[key] + .get::() + .copied() + .with_context(|| format!("invalid value for `{key}`")) +} + +fn get_json_int(value: &tinyjson::JsonValue, key: &str) -> anyhow::Result { + #[expect( + clippy::as_conversions, + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + reason = "tinyjson does not expose any integers at all, so we need the f64 to u64 as casting" + )] + get_json_float(value, key).map(|value| value as u64) +} diff --git a/xtask/src/ffi.rs b/xtask/src/ffi.rs new file mode 100644 index 00000000..b8e40e7b --- /dev/null +++ b/xtask/src/ffi.rs @@ -0,0 +1,130 @@ +use std::fs::{self, create_dir_all}; +use std::path::{Path, PathBuf}; + +use anyhow::Context as _; + +use crate::prelude::*; + +#[cfg(target_os = "windows")] +const OUTPUT_LIB_NAME: &str = "ironrdp.dll"; +#[cfg(target_os = "linux")] +const OUTPUT_LIB_NAME: &str = "libironrdp.so"; +#[cfg(target_os = "macos")] +const OUTPUT_LIB_NAME: &str = "libironrdp.dylib"; + +#[cfg(target_os = "windows")] +const DOTNET_NATIVE_LIB_NAME: &str = "DevolutionsIronRdp.dll"; +#[cfg(target_os = "linux")] +const DOTNET_NATIVE_LIB_NAME: &str = "libDevolutionsIronRdp.so"; +#[cfg(target_os = "macos")] +const DOTNET_NATIVE_LIB_NAME: &str = "libDevolutionsIronRdp.dylib"; + +#[cfg(target_os = "windows")] +const DOTNET_NATIVE_LIB_PATH: &str = "dependencies/runtimes/win-x64/native/"; +#[cfg(target_os = "linux")] +const DOTNET_NATIVE_LIB_PATH: &str = "dependencies/runtimes/linux-x64/native/"; +#[cfg(target_os = "macos")] +const DOTNET_NATIVE_LIB_PATH: &str = "dependencies/runtimes/osx-x64/native/"; + +pub(crate) fn install(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("FFI-INSTALL"); + + cargo_install(sh, &DIPLOMAT_TOOL)?; + + Ok(()) +} + +pub(crate) fn build_dynamic_lib(sh: &Shell, release: bool) -> anyhow::Result<()> { + let _s = Section::new("BUILD-DYNAMIC-LIBRARY"); + + println!("Build IronRDP DLL"); + + let mut args = vec!["build", "--package", "ffi"]; + if release { + args.push("--release"); + } + sh.cmd("cargo").args(&args).run()?; + + let profile_dir = if release { "release" } else { "debug" }; + + let root_dir = sh.current_dir(); + let target_dir = root_dir.join("target"); + let profile_dir = target_dir.join(profile_dir); + + let output_lib_path = profile_dir.join(OUTPUT_LIB_NAME); + + let dotnet_native_lib_dir_path: PathBuf = DOTNET_NATIVE_LIB_PATH.parse()?; + let dotnet_native_lib_path = root_dir.join(&dotnet_native_lib_dir_path).join(DOTNET_NATIVE_LIB_NAME); + + create_dir_all(&dotnet_native_lib_dir_path) + .with_context(|| format!("failed to create directory {}", dotnet_native_lib_dir_path.display()))?; + + fs::copy(&output_lib_path, &dotnet_native_lib_path).with_context(|| { + format!( + "failed to copy {} to {}", + output_lib_path.display(), + dotnet_native_lib_path.display() + ) + })?; + + println!( + "Copied {} to {}", + output_lib_path.display(), + dotnet_native_lib_path.display() + ); + + Ok(()) +} + +pub(crate) fn build_bindings(sh: &Shell, skip_dotnet_build: bool) -> anyhow::Result<()> { + let _s = Section::new("BUILD-BINDINGS"); + + if !is_installed(sh, "diplomat-tool") { + anyhow::bail!("`diplomat-tool` binary is missing. Please run `cargo xtask ffi install`."); + } + + let dotnet_generated_path = "./dotnet/Devolutions.IronRdp/Generated/"; + let diplomat_config = "./dotnet-interop-conf.toml"; + + // Check if diplomat tool is installed + sh.change_dir("./ffi"); + let cwd = sh.current_dir(); + let generated_code_dir = cwd.join(dotnet_generated_path); + if !generated_code_dir.exists() { + anyhow::bail!("The directory {} does not exist", generated_code_dir.display()); + } + remove_cs_files(&generated_code_dir)?; + + sh.cmd("diplomat-tool") + .arg("dotnet") + .arg(dotnet_generated_path) + .arg("-l") + .arg(diplomat_config) + .run()?; + + if skip_dotnet_build { + return Ok(()); + } + + sh.change_dir("./dotnet/Devolutions.IronRdp/"); + + cmd!(sh, "dotnet build").run()?; + + Ok(()) +} + +/// Removes all `.cs` files in the given directory. +fn remove_cs_files(dir: &Path) -> anyhow::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("cs") { + println!("Removing file: {path:?}"); + fs::remove_file(path)?; + } + } + } + + Ok(()) +} diff --git a/xtask/src/fuzz.rs b/xtask/src/fuzz.rs new file mode 100644 index 00000000..f87c9c80 --- /dev/null +++ b/xtask/src/fuzz.rs @@ -0,0 +1,94 @@ +use crate::prelude::*; +// NOTE: cargo-fuzz (libFuzzer) does not support Windows yet (coming soon?) + +pub fn corpus_minify(sh: &Shell, target: Option) -> anyhow::Result<()> { + let _s = Section::new("FUZZ-CORPUS-MINIFY"); + windows_skip!(); + + let _guard = sh.push_dir("./fuzz"); + + let target_from_user = target.as_deref().map(|value| [value]); + + let targets = if let Some(targets) = &target_from_user { + targets + } else { + FUZZ_TARGETS + }; + + for target in targets { + cmd!(sh, "rustup run {NIGHTLY_TOOLCHAIN} cargo fuzz cmin {target}").run()?; + } + + Ok(()) +} + +pub fn corpus_fetch(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("FUZZ-CORPUS-FETCH"); + windows_skip!(); + + cmd!( + sh, + "az storage blob download-batch --account-name fuzzingcorpus --source ironrdp --destination fuzz --output none" + ) + .run()?; + + Ok(()) +} + +pub fn corpus_push(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("FUZZ-CORPUS-PUSH"); + windows_skip!(); + + cmd!( + sh, + "az storage blob sync --account-name fuzzingcorpus --container ironrdp --source fuzz/corpus --destination corpus --delete-destination true --output none" + ) + .run()?; + + cmd!( + sh, + "az storage blob sync --account-name fuzzingcorpus --container ironrdp --source fuzz/artifacts --destination artifacts --delete-destination true --output none" + ) + .run()?; + + Ok(()) +} + +pub fn install(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("FUZZ-INSTALL"); + windows_skip!(); + + cargo_install(sh, &CARGO_FUZZ)?; + + cmd!(sh, "rustup install {NIGHTLY_TOOLCHAIN} --profile=minimal").run()?; + + Ok(()) +} + +pub fn run(sh: &Shell, duration: Option, target: Option) -> anyhow::Result<()> { + let _s = Section::new("FUZZ-RUN"); + windows_skip!(); + + let _guard = sh.push_dir("./fuzz"); + + let duration = duration.unwrap_or(5).to_string(); + let target_from_user = target.as_deref().map(|value| [value]); + + let targets = if let Some(targets) = &target_from_user { + targets + } else { + FUZZ_TARGETS + }; + + for target in targets { + cmd!( + sh, + "rustup run {NIGHTLY_TOOLCHAIN} cargo fuzz run {target} -- -max_total_time={duration}" + ) + .run()?; + } + + println!("All good!"); + + Ok(()) +} diff --git a/xtask/src/macros.rs b/xtask/src/macros.rs new file mode 100644 index 00000000..d9d747d3 --- /dev/null +++ b/xtask/src/macros.rs @@ -0,0 +1,30 @@ +macro_rules! windows_skip { + () => { + if cfg!(target_os = "windows") { + eprintln!("Skip (unsupported on windows)"); + return Ok(()); + } + }; +} + +pub(crate) use windows_skip; + +macro_rules! trace { + ($($arg:tt)*) => {{ + if $crate::is_verbose() { + eprintln!($($arg)*); + } + }}; +} + +pub(crate) use trace; + +macro_rules! run_cmd_in { + ($sh:expr, $prefix:expr, $args:literal) => {{ + let _guard = $sh.push_dir($prefix); + eprintln!("In {}:", $sh.current_dir().display()); + ::xshell::cmd!($sh, $args).run() + }}; +} + +pub(crate) use run_cmd_in; diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..cf422028 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,211 @@ +#![allow(clippy::print_stdout)] +#![allow(clippy::print_stderr)] +#![allow(unreachable_pub)] + +mod macros; + +mod bin_install; +mod bin_version; +mod check; +mod clean; +mod cli; +mod cov; +mod ffi; +mod fuzz; +mod prelude; +mod section; +mod wasm; +mod web; + +use std::path::{Path, PathBuf}; + +use xshell::Shell; + +use crate::cli::Action; +use crate::macros::trace; + +#[cfg(target_os = "windows")] +pub const LOCAL_CARGO_ROOT: &str = ".cargo\\local_root\\"; +#[cfg(not(target_os = "windows"))] +pub const LOCAL_CARGO_ROOT: &str = ".cargo/local_root/"; + +pub const CARGO: &str = env!("CARGO"); + +pub const WASM_PACKAGES: &[&str] = &["ironrdp-web"]; + +pub const FUZZ_TARGETS: &[&str] = &[ + "pdu_decoding", + "rle_decompression", + "bitmap_stream", + "cliprdr_format", + "channel_processing", +]; + +fn main() -> anyhow::Result<()> { + let args = match cli::parse_args() { + Ok(args) => args, + Err(e) => { + cli::print_help(); + return Err(e); + } + }; + + set_verbose(args.verbose); + + let sh = new_shell()?; + + match args.action { + Action::ShowHelp => cli::print_help(), + Action::Bootstrap => { + check::install(&sh)?; + cov::install(&sh)?; + fuzz::install(&sh)?; + wasm::install(&sh)?; + web::install(&sh)?; + + if is_verbose() { + list_files(&sh, local_bin())?; + } + } + Action::CheckFmt => check::fmt(&sh)?, + Action::CheckLints => check::lints(&sh)?, + Action::CheckLocks => check::lock_files(&sh)?, + Action::CheckTests { no_run } => { + if no_run { + check::tests_compile(&sh)?; + } else { + check::tests_run(&sh)?; + } + } + Action::CheckTypos => { + check::typos(&sh)?; + } + Action::CheckInstall => { + check::install(&sh)?; + } + Action::Ci => { + check::fmt(&sh)?; + check::typos(&sh)?; + check::tests_compile(&sh)?; + check::tests_run(&sh)?; + check::lints(&sh)?; + wasm::check(&sh)?; + fuzz::run(&sh, None, None)?; + web::install(&sh)?; + web::check(&sh)?; + check::lock_files(&sh)?; + } + Action::Clean => clean::workspace(&sh)?, + Action::CovGrcov => cov::grcov(&sh)?, + Action::CovInstall => cov::install(&sh)?, + Action::CovReportGitHub { repo, pr } => cov::report_github(&sh, &repo, pr)?, + Action::CovReport { html_report } => cov::report(&sh, html_report)?, + Action::CovUpdate => cov::update(&sh)?, + Action::FuzzCorpusFetch => fuzz::corpus_fetch(&sh)?, + Action::FuzzCorpusMin { target } => fuzz::corpus_minify(&sh, target)?, + Action::FuzzCorpusPush => fuzz::corpus_push(&sh)?, + Action::FuzzInstall => fuzz::install(&sh)?, + Action::FuzzRun { duration, target } => fuzz::run(&sh, duration, target)?, + Action::WasmCheck => wasm::check(&sh)?, + Action::WasmInstall => wasm::install(&sh)?, + Action::WebCheck => web::check(&sh)?, + Action::WebBuild => web::build(&sh, false)?, + Action::WebInstall => web::install(&sh)?, + Action::WebRun => web::run(&sh)?, + Action::FfiInstall => ffi::install(&sh)?, + Action::FfiBuildDll { release } => ffi::build_dynamic_lib(&sh, release)?, + Action::FfiBuildBindings { skip_dotnet_build } => ffi::build_bindings(&sh, skip_dotnet_build)?, + } + + Ok(()) +} + +fn new_shell() -> anyhow::Result { + let sh = Shell::new()?; + + sh.change_dir(project_root()); + create_folders(&sh)?; + update_env_path(&sh)?; + + Ok(sh) +} + +fn project_root() -> PathBuf { + Path::new(&env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(1) + .expect("failed to retrieve project root path") + .to_path_buf() +} + +fn update_env_path(sh: &Shell) -> anyhow::Result<()> { + use anyhow::Context as _; + + let original_path = sh.var_os("PATH").context("PATH variable")?; + + let paths_to_add = vec![sh.current_dir().join(local_bin())]; + + let mut new_path = std::ffi::OsString::new(); + + for path in paths_to_add { + trace!("Add {} to PATH", path.display()); + new_path.push(path.as_os_str()); + + #[cfg(target_os = "windows")] + new_path.push(";"); + #[cfg(not(target_os = "windows"))] + new_path.push(":"); + } + + new_path.push(original_path); + trace!("New PATH: {}", new_path.to_string_lossy()); + + sh.set_var("PATH", new_path); + + Ok(()) +} + +fn create_folders(sh: &Shell) -> anyhow::Result<()> { + use anyhow::Context as _; + + sh.create_dir(LOCAL_CARGO_ROOT) + .context(format!("create directory: {LOCAL_CARGO_ROOT}"))?; + + let local_bin = local_bin(); + sh.create_dir(&local_bin) + .context(format!("create directory: {}", local_bin.display()))?; + + Ok(()) +} + +pub fn local_cargo_root() -> PathBuf { + PathBuf::from(LOCAL_CARGO_ROOT) +} + +pub fn local_bin() -> PathBuf { + let mut path = local_cargo_root(); + path.push("bin"); + path +} + +static VERBOSE: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); + +pub fn set_verbose(value: bool) { + VERBOSE.store(value, core::sync::atomic::Ordering::Release); +} + +pub fn is_verbose() -> bool { + VERBOSE.load(core::sync::atomic::Ordering::Acquire) +} + +pub fn list_files(sh: &Shell, path: impl AsRef) -> anyhow::Result<()> { + let path = path.as_ref(); + + eprintln!("Listing folder {}:", path.display()); + + for file in sh.read_dir(path)? { + eprintln!("- {}", file.display()); + } + + Ok(()) +} diff --git a/xtask/src/prelude.rs b/xtask/src/prelude.rs new file mode 100644 index 00000000..ce847146 --- /dev/null +++ b/xtask/src/prelude.rs @@ -0,0 +1,8 @@ +pub use anyhow::Context as _; +pub use xshell::{cmd, Shell}; + +pub use crate::bin_install::{cargo_install, is_installed}; +pub use crate::bin_version::*; +pub(crate) use crate::macros::{run_cmd_in, trace, windows_skip}; +pub use crate::section::Section; +pub use crate::{is_verbose, list_files, CARGO, FUZZ_TARGETS, WASM_PACKAGES}; diff --git a/xtask/src/section.rs b/xtask/src/section.rs new file mode 100644 index 00000000..cf5ce59b --- /dev/null +++ b/xtask/src/section.rs @@ -0,0 +1,30 @@ +use std::time::Instant; + +pub struct Section { + name: &'static str, + start: Instant, +} + +impl Section { + pub fn new(name: &'static str) -> Section { + flush_all(); + eprintln!("::group::{name}"); + let start = Instant::now(); + Self { name, start } + } +} + +impl Drop for Section { + fn drop(&mut self) { + flush_all(); + eprintln!("{}: {:.2?}", self.name, self.start.elapsed()); + eprintln!("::endgroup::"); + } +} + +fn flush_all() { + use std::io::{self, Write as _}; + + let _ = io::stdout().flush(); + let _ = io::stderr().flush(); +} diff --git a/xtask/src/wasm.rs b/xtask/src/wasm.rs new file mode 100644 index 00000000..648f11ea --- /dev/null +++ b/xtask/src/wasm.rs @@ -0,0 +1,103 @@ +use crate::local_bin; +use crate::prelude::*; + +pub fn check(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("WASM-CHECK"); + + for package in WASM_PACKAGES { + println!("Check {package}"); + + cmd!( + sh, + "{CARGO} rustc --locked --target wasm32-unknown-unknown --package {package} --lib --crate-type cdylib" + ) + .run()?; + + // When building a library, `-` in the artifact name are replaced by `_` + let artifact_name = format!("{}.wasm", package.replace('-', "_")); + + if is_verbose() { + cmd!(sh, "wasm2wat --version").run()?; + list_files(sh, "./target/")?; + list_files(sh, "./target/wasm32-unknown-unknown/debug/")?; + list_files(sh, local_bin())?; + } + + let stdout = cmd!(sh, "wasm2wat ./target/wasm32-unknown-unknown/debug/{artifact_name}").read()?; + + if stdout.contains("import \"env\"") { + anyhow::bail!("Found undefined symbols in generated wasm file"); + } + } + + println!("All good!"); + + Ok(()) +} + +pub fn install(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("WASM-INSTALL"); + + cmd!(sh, "rustup target add wasm32-unknown-unknown").run()?; + + match cmd!(sh, "wasm2wat --version").read() { + Ok(version) => println!("Found wasm2wat {version}"), + Err(e) => { + trace!("{e}"); + install_wasm2wat(sh)?; + } + } + + Ok(()) +} + +fn install_wasm2wat(sh: &Shell) -> anyhow::Result<()> { + println!("Installing wasm2wat in local root..."); + + let _guard = sh.push_dir(crate::LOCAL_CARGO_ROOT); + + let platform_suffix = if cfg!(target_os = "windows") { + "windows" + } else if cfg!(target_os = "macos") { + "macos-14" + } else { + "ubuntu-20.04" + }; + + let url = format!( + "https://github.com/WebAssembly/wabt/releases/download/{WABT_VERSION}/wabt-{WABT_VERSION}-{platform_suffix}.tar.gz" + ); + + cmd!(sh, "curl --location --remote-header-name {url} --output wabt.tar.gz").run()?; + + if is_verbose() { + list_files(sh, ".")?; + } + + sh.create_dir("wabt")?; + cmd!(sh, "tar xf wabt.tar.gz -C ./wabt --strip-components 1").run()?; + + if is_verbose() { + list_files(sh, "./wabt")?; + list_files(sh, "./wabt/bin")?; + } + + trace!("Copy wasm2wat to local bin"); + + if cfg!(target_os = "windows") { + sh.copy_file("./wabt/bin/wasm2wat.exe", "./bin/wasm2wat.exe")?; + } else { + sh.copy_file("./wabt/bin/wasm2wat", "./bin/wasm2wat")?; + } + + trace!("Clean artifacts"); + + sh.remove_path("./wabt.tar.gz")?; + sh.remove_path("./wabt")?; + + if is_verbose() { + list_files(sh, ".")?; + } + + Ok(()) +} diff --git a/xtask/src/web.rs b/xtask/src/web.rs new file mode 100644 index 00000000..efad2efb --- /dev/null +++ b/xtask/src/web.rs @@ -0,0 +1,78 @@ +use std::fs; + +use crate::prelude::*; + +const IRON_REMOTE_DESKTOP_PATH: &str = "./web-client/iron-remote-desktop"; +const IRON_REMOTE_DESKTOP_RDP_PATH: &str = "./web-client/iron-remote-desktop-rdp"; +const IRON_SVELTE_CLIENT_PATH: &str = "./web-client/iron-svelte-client"; +const IRONRDP_WEB_PATH: &str = "./crates/ironrdp-web"; +const IRONRDP_WEB_PACKAGE_JS_PATH: &str = "./crates/ironrdp-web/pkg/ironrdp_web.js"; + +#[cfg(not(target_os = "windows"))] +const NPM: &str = "npm"; +#[cfg(target_os = "windows")] +const NPM: &str = "npm.cmd"; + +pub fn install(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("WEB-INSTALL"); + + run_cmd_in!(sh, IRON_REMOTE_DESKTOP_PATH, "{NPM} install")?; + run_cmd_in!(sh, IRON_REMOTE_DESKTOP_RDP_PATH, "{NPM} install")?; + run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} install")?; + + cargo_install(sh, &WASM_PACK)?; + + Ok(()) +} + +pub fn check(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("WEB-CHECK"); + + build(sh, true)?; + + run_cmd_in!(sh, IRON_REMOTE_DESKTOP_PATH, "{NPM} run check")?; + run_cmd_in!(sh, IRON_REMOTE_DESKTOP_PATH, "{NPM} run lint")?; + run_cmd_in!(sh, IRON_REMOTE_DESKTOP_RDP_PATH, "{NPM} run check")?; + run_cmd_in!(sh, IRON_REMOTE_DESKTOP_RDP_PATH, "{NPM} run lint")?; + run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} run check")?; + run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} run lint")?; + + Ok(()) +} + +pub fn run(sh: &Shell) -> anyhow::Result<()> { + let _s = Section::new("WEB-RUN"); + + run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} run dev-no-wasm")?; + + Ok(()) +} + +pub fn build(sh: &Shell, wasm_pack_dev: bool) -> anyhow::Result<()> { + if wasm_pack_dev { + run_cmd_in!(sh, IRONRDP_WEB_PATH, "wasm-pack build --dev --target web")?; + } else { + let _env_guard = sh.push_env( + "RUSTFLAGS", + "-Ctarget-feature=+simd128,+bulk-memory --cfg getrandom_backend=\"wasm_js\"", + ); + run_cmd_in!(sh, IRONRDP_WEB_PATH, "wasm-pack build --target web")?; + } + + let ironrdp_web_js_file_path = sh.current_dir().join(IRONRDP_WEB_PACKAGE_JS_PATH); + + let ironrdp_web_js_content = fs::read_to_string(&ironrdp_web_js_file_path)?; + + // Modify the js file to get rid of the `URL` object. + // Vite doesn't work properly with inlined urls in `new URL(url, import.meta.url)`. + let ironrdp_web_js_content = + format!("import wasmUrl from './ironrdp_web_bg.wasm?url';\n\n{ironrdp_web_js_content}"); + let ironrdp_web_js_content = + ironrdp_web_js_content.replace("new URL('ironrdp_web_bg.wasm', import.meta.url)", "wasmUrl"); + + fs::write(&ironrdp_web_js_file_path, ironrdp_web_js_content)?; + + run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} run build-no-wasm")?; + + Ok(()) +}