fix(windows): restore Windows 7 startup compatibility

This commit is contained in:
zipg 2026-08-05 00:07:41 +08:00 committed by GitHub
parent 4aa36cb893
commit e82c1aedf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 17157 additions and 66 deletions

View File

@ -0,0 +1,56 @@
[CmdletBinding()]
param(
[string]$RuntimeDirectory = (Join-Path $PSScriptRoot "..\..\src-tauri\webview2-fixed-runtime"),
[string]$LoaderPath = (Join-Path ([System.IO.Path]::GetTempPath()) "dbx-win7-webview2-loader-probe\WebView2Loader.dll"),
[string]$ExpectedVersion = "109.0.1518.78"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$runtimeDirectory = (Resolve-Path -LiteralPath $RuntimeDirectory).Path
$runtimeExecutable = Join-Path $runtimeDirectory "msedgewebview2.exe"
if (!(Test-Path -LiteralPath $runtimeExecutable -PathType Leaf)) {
throw "WebView2 fixed runtime executable does not exist: $runtimeExecutable"
}
$loaderPath = (Resolve-Path -LiteralPath $LoaderPath).Path
$escapedLoaderPath = $loaderPath.Replace('"', '""')
$source = @"
using System;
using System.Runtime.InteropServices;
public static class DbxWebView2LoaderProbe
{
[DllImport(@"$escapedLoaderPath", CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern int GetAvailableCoreWebView2BrowserVersionString(
string browserExecutableFolder,
out IntPtr versionInfo);
}
"@
Add-Type -TypeDefinition $source -Language CSharp
$versionPointer = [IntPtr]::Zero
$result = [DbxWebView2LoaderProbe]::GetAvailableCoreWebView2BrowserVersionString(
$runtimeDirectory,
[ref]$versionPointer
)
if ($result -ne 0) {
throw "WebView2 loader failed to recognize fixed runtime at $runtimeDirectory (HRESULT 0x$($result.ToString('X8')))."
}
if ($versionPointer -eq [IntPtr]::Zero) {
throw "WebView2 loader returned an empty version pointer for $runtimeDirectory."
}
try {
$version = [Runtime.InteropServices.Marshal]::PtrToStringUni($versionPointer)
}
finally {
[Runtime.InteropServices.Marshal]::FreeCoTaskMem($versionPointer)
}
if ([string]::IsNullOrWhiteSpace($version) -or !$version.StartsWith($ExpectedVersion)) {
throw "Expected WebView2 fixed runtime $ExpectedVersion, detected '$version'."
}
Write-Host "WebView2 fixed runtime probe passed: loader=$loaderPath runtime=$runtimeDirectory version=$version"

View File

@ -0,0 +1,38 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$InstallerPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$installerPath = (Resolve-Path -LiteralPath $InstallerPath).Path
$installDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "dbx-win7-installer-audit"
if (Test-Path -LiteralPath $installDirectory) {
Remove-Item -LiteralPath $installDirectory -Recurse -Force
}
$installer = Start-Process -FilePath $installerPath -ArgumentList @("/S", "/D=$installDirectory") -Wait -PassThru
if ($installer.ExitCode -ne 0) {
throw "Windows 7 test installer failed with exit code $($installer.ExitCode)."
}
$expectedFiles = @(
(Join-Path $installDirectory "dbx.exe"),
(Join-Path $installDirectory "webview2-fixed-runtime\msedgewebview2.exe"),
(Join-Path $installDirectory "uninstall.exe")
)
foreach ($path in $expectedFiles) {
if (!(Test-Path -LiteralPath $path -PathType Leaf)) {
throw "Windows 7 test installer omitted required file: $path"
}
}
Write-Host "Windows 7 installer content audit passed: $installerPath"
$uninstallerPath = Join-Path $installDirectory "uninstall.exe"
$uninstaller = Start-Process -FilePath $uninstallerPath -ArgumentList @("/S", "_?=$installDirectory") -Wait -PassThru
if ($uninstaller.ExitCode -ne 0) {
Write-Warning "Windows 7 test uninstaller returned exit code $($uninstaller.ExitCode)."
}

View File

@ -0,0 +1,71 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$BinaryPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
if (!(Test-Path -LiteralPath $BinaryPath -PathType Leaf)) {
throw "Windows 7 PE audit target does not exist: $BinaryPath"
}
$dumpbinCommand = Get-Command dumpbin.exe -ErrorAction SilentlyContinue
$dumpbinPath = if ($null -ne $dumpbinCommand) { $dumpbinCommand.Source } else { $null }
if ($null -eq $dumpbinPath) {
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path -LiteralPath $vswhere) {
$visualStudio = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
if ($visualStudio) {
$dumpbin = Get-ChildItem (Join-Path $visualStudio "VC\Tools\MSVC") -Filter dumpbin.exe -Recurse |
Where-Object { $_.FullName -match '\\bin\\Hostx64\\x64\\dumpbin\.exe$' } |
Sort-Object FullName -Descending |
Select-Object -First 1
if ($null -ne $dumpbin) {
$dumpbinPath = $dumpbin.FullName
}
}
}
}
if ($null -eq $dumpbinPath) {
throw "Unable to find dumpbin.exe for the Windows 7 PE compatibility audit."
}
$imports = (& $dumpbinPath /nologo /imports $BinaryPath 2>&1 | Out-String)
if ($LASTEXITCODE -ne 0) {
throw "dumpbin failed while auditing ${BinaryPath}:`n$imports"
}
$forbiddenImports = [ordered]@{
"combase.dll" = "COMBASE is only available starting with Windows 8; use OLE32 imports."
"api-ms-win-core-winrt-" = "WinRT API sets are unavailable on Windows 7."
"CoIncrementMTAUsage" = "CoIncrementMTAUsage is unavailable on Windows 7."
"EventSetInformation" = "EventSetInformation is unavailable on Windows 7. Use the legacy WebView2 loader."
"GetSystemTimePreciseAsFileTime" = "GetSystemTimePreciseAsFileTime is unavailable on Windows 7."
"GetDpiForWindow" = "GetDpiForWindow is unavailable on Windows 7."
"GetSystemMetricsForDpi" = "GetSystemMetricsForDpi is unavailable on Windows 7."
"SetThreadDpiAwarenessContext" = "SetThreadDpiAwarenessContext is unavailable on Windows 7."
"VCRUNTIME140.dll" = "The Windows 7 package must not require a separately installed VC++ Runtime."
"VCRUNTIME140_1.dll" = "The Windows 7 package must not require a separately installed VC++ Runtime."
"MSVCP140.dll" = "The Windows 7 package must not require a separately installed VC++ Runtime."
"ucrtbase.dll" = "The Windows 7 package must link the Universal CRT statically."
"api-ms-win-crt-" = "The Windows 7 package must not require separately installed Universal CRT API sets."
}
$violations = @()
foreach ($entry in $forbiddenImports.GetEnumerator()) {
if ($imports -match [regex]::Escape($entry.Key)) {
$violations += "$($entry.Key): $($entry.Value)"
}
}
if ($violations.Count -gt 0) {
$summary = $violations -join "`n"
Write-Host "Full PE import table for diagnosis:"
Write-Host $imports
throw "Windows 7 incompatible PE imports detected in ${BinaryPath}:`n$summary"
}
Write-Host "Windows 7 PE import audit passed: $BinaryPath"

View File

@ -0,0 +1,106 @@
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Newer WebView2 static loaders import EventSetInformation, which does not exist on Windows 7.
# The loader entry points are stable, so the Win7 bundle uses the last verified compatible SDK loader.
$sdkVersion = "1.0.1054.31"
$sdkPackageSha256 = "0afe683aa3d143a5f6330db1ce833c69278b38fe5e1eadec52f26910ad26e22f"
$loaderSha256 = "76314119685bbf4c2b2423a44e81b57beadc914c943d0e772fd6bc78c8e6b0e8"
$webView2ComSysVersion = "0.38.2"
$upstreamLoaderSha256 = "0659b741bde6348d4c4a6ec4ceb9af50e3d0048ed9cd3c8659bccbb61fde55ee"
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot "../..")).Path
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) "dbx-win7-webview2-loader-$([Guid]::NewGuid())"
$packagePath = Join-Path $temporaryRoot "Microsoft.Web.WebView2.$sdkVersion.nupkg"
$extractedPath = Join-Path $temporaryRoot "extracted"
try {
New-Item -ItemType Directory -Path $extractedPath -Force | Out-Null
$packageUrl = "https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/$sdkVersion"
Write-Host "Downloading WebView2 SDK $sdkVersion for the Windows 7 loader..."
Invoke-WebRequest -Uri $packageUrl -OutFile $packagePath -UseBasicParsing
$actualPackageSha256 = (Get-FileHash -LiteralPath $packagePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualPackageSha256 -ne $sdkPackageSha256) {
throw "Unexpected WebView2 SDK package SHA256: $actualPackageSha256"
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($packagePath, $extractedPath)
$legacyLoader = Join-Path $extractedPath "build/native/x64/WebView2LoaderStatic.lib"
if (!(Test-Path -LiteralPath $legacyLoader -PathType Leaf)) {
throw "WebView2 SDK $sdkVersion does not contain the x64 static loader."
}
$legacyLoaderDll = Join-Path $extractedPath "build/native/x64/WebView2Loader.dll"
if (!(Test-Path -LiteralPath $legacyLoaderDll -PathType Leaf)) {
throw "WebView2 SDK $sdkVersion does not contain the x64 loader DLL."
}
$actualLoaderSha256 = (Get-FileHash -LiteralPath $legacyLoader -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualLoaderSha256 -ne $loaderSha256) {
throw "Unexpected Windows 7 WebView2 loader SHA256: $actualLoaderSha256"
}
Push-Location $repositoryRoot
try {
& cargo fetch --locked --target x86_64-win7-windows-msvc
if ($LASTEXITCODE -ne 0) {
throw "cargo fetch failed while preparing the Windows 7 WebView2 loader."
}
$metadataJson = & cargo metadata --locked --format-version 1
if ($LASTEXITCODE -ne 0) {
throw "cargo metadata failed while locating webview2-com-sys."
}
}
finally {
Pop-Location
}
$metadata = $metadataJson | ConvertFrom-Json
$webView2Packages = @($metadata.packages | Where-Object {
$_.name -eq "webview2-com-sys" -and $_.version -eq $webView2ComSysVersion
})
if ($webView2Packages.Count -ne 1) {
throw "Expected exactly one webview2-com-sys $webView2ComSysVersion package, found $($webView2Packages.Count)."
}
$crateRoot = Split-Path -Parent $webView2Packages[0].manifest_path
$loaderDestination = Join-Path $crateRoot "x64/WebView2LoaderStatic.lib"
if (!(Test-Path -LiteralPath $loaderDestination -PathType Leaf)) {
throw "webview2-com-sys static loader does not exist: $loaderDestination"
}
$existingLoaderSha256 = (Get-FileHash -LiteralPath $loaderDestination -Algorithm SHA256).Hash.ToLowerInvariant()
$knownLoaderHashes = @($upstreamLoaderSha256, $loaderSha256)
if ($existingLoaderSha256 -notin $knownLoaderHashes) {
throw "Refusing to replace an unknown webview2-com-sys loader SHA256: $existingLoaderSha256"
}
Set-ItemProperty -LiteralPath $loaderDestination -Name IsReadOnly -Value $false
Copy-Item -LiteralPath $legacyLoader -Destination $loaderDestination -Force
$installedLoaderSha256 = (Get-FileHash -LiteralPath $loaderDestination -Algorithm SHA256).Hash.ToLowerInvariant()
if ($installedLoaderSha256 -ne $loaderSha256) {
throw "Windows 7 WebView2 loader replacement failed: $installedLoaderSha256"
}
$probeDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "dbx-win7-webview2-loader-probe"
New-Item -ItemType Directory -Path $probeDirectory -Force | Out-Null
$probeLoader = Join-Path $probeDirectory "WebView2Loader.dll"
Copy-Item -LiteralPath $legacyLoaderDll -Destination $probeLoader -Force
Write-Host "Prepared WebView2 SDK $sdkVersion static loader for Windows 7: $loaderDestination"
Write-Host "Prepared WebView2 SDK $sdkVersion loader probe DLL: $probeLoader"
}
finally {
if (Test-Path -LiteralPath $temporaryRoot) {
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force
}
}

View File

@ -1,65 +1,76 @@
[CmdletBinding()]
param(
[string]$CacheRoot = (Join-Path $env:LOCALAPPDATA "tauri"),
[string]$RuntimeDirectory = (Join-Path $PSScriptRoot "..\..\src-tauri\webview2-fixed-runtime"),
[string]$DownloadDirectory = $env:RUNNER_TEMP
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$runtimeVersion = "109.0.1518.140"
$runtimeUrl = "https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/updt/2023/09/microsoftedgestandaloneinstallerx64_1c890b4b8dd6b7c93da98ebdc08ecdc5e30e50cb.exe"
$runtimeSha256 = "eac95c8095ec5f9971eade9827d8fb67fd251f5c16e702b5312d31067e39119b"
$evergreenUrl = "https://go.microsoft.com/fwlink/?linkid=2124701"
$runtimeVersion = "109.0.1518.78"
$runtimeFolderName = "Microsoft.WebView2.FixedVersionRuntime.$runtimeVersion.x64"
$archiveName = "$runtimeFolderName.cab"
$runtimeUrl = "https://github.com/westinyang/WebView2RuntimeArchive/releases/download/$runtimeVersion/$archiveName"
$runtimeSha256 = "7622281cf83de1a35e3a471f432f7a897d65f0a7d3975df08512b7b253dd45c7"
if ([string]::IsNullOrWhiteSpace($CacheRoot)) {
throw "A Tauri cache root is required."
if ([string]::IsNullOrWhiteSpace($RuntimeDirectory)) {
throw "A WebView2 fixed runtime directory is required."
}
if ([string]::IsNullOrWhiteSpace($DownloadDirectory)) {
$DownloadDirectory = [System.IO.Path]::GetTempPath()
}
New-Item -ItemType Directory -Force -Path $DownloadDirectory | Out-Null
$downloadPath = Join-Path $DownloadDirectory "MicrosoftEdgeWebView2Runtime-$runtimeVersion-x64.exe"
$archivePath = Join-Path $DownloadDirectory $archiveName
if (Test-Path $downloadPath) {
$downloadHash = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash.ToLowerInvariant()
if (Test-Path $archivePath) {
$downloadHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($downloadHash -ne $runtimeSha256) {
Remove-Item -LiteralPath $downloadPath -Force
Remove-Item -LiteralPath $archivePath -Force
}
}
if (!(Test-Path $downloadPath)) {
Write-Host "Downloading WebView2 Runtime $runtimeVersion for Windows 7..."
Invoke-WebRequest -Uri $runtimeUrl -OutFile $downloadPath
if (!(Test-Path $archivePath)) {
Write-Host "Downloading WebView2 fixed runtime $runtimeVersion for Windows 7..."
Invoke-WebRequest -Uri $runtimeUrl -OutFile $archivePath
}
$actualHash = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash.ToLowerInvariant()
$actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualHash -ne $runtimeSha256) {
throw "WebView2 Runtime SHA-256 mismatch. Expected $runtimeSha256, got $actualHash."
throw "WebView2 fixed runtime SHA-256 mismatch. Expected $runtimeSha256, got $actualHash."
}
# Tauri 2.11 does not expose an offline-installer path override. It resolves the
# Evergreen URL and reuses a matching cache entry, so place the verified 109
# installer at that exact location before bundling.
$response = Invoke-WebRequest -Uri $evergreenUrl -Method Head
$resolvedUrl = $response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
$match = [regex]::Match(
$resolvedUrl,
"/filestreamingservice/files/(?<guid>[^/]+)/(?<filename>[^/?]+)"
)
if (!$match.Success) {
throw "Unexpected Evergreen WebView2 URL: $resolvedUrl"
# Microsoft no longer publishes old Fixed Version downloads. The archive is
# accepted only when both its pinned hash and original Microsoft signature match.
$signature = Get-AuthenticodeSignature -LiteralPath $archivePath
if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or
$null -eq $signature.SignerCertificate -or
$signature.SignerCertificate.Subject -notmatch "Microsoft Corporation") {
throw "WebView2 fixed runtime does not have a valid Microsoft signature."
}
$cacheDirectory = Join-Path $CacheRoot (Join-Path "x64" $match.Groups["guid"].Value)
$cachePath = Join-Path $cacheDirectory $match.Groups["filename"].Value
New-Item -ItemType Directory -Force -Path $cacheDirectory | Out-Null
Copy-Item -LiteralPath $downloadPath -Destination $cachePath -Force
$extractDirectory = Join-Path $DownloadDirectory "dbx-webview2-fixed-runtime-$runtimeVersion"
if (Test-Path $extractDirectory) {
Remove-Item -LiteralPath $extractDirectory -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $extractDirectory | Out-Null
$cacheHash = (Get-FileHash -LiteralPath $cachePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($cacheHash -ne $runtimeSha256) {
throw "Cached WebView2 Runtime SHA-256 mismatch. Expected $runtimeSha256, got $cacheHash."
$expand = Join-Path $env:SystemRoot "System32\expand.exe"
& $expand $archivePath "-F:*" $extractDirectory
if ($LASTEXITCODE -ne 0) {
throw "Failed to extract WebView2 fixed runtime archive (exit code $LASTEXITCODE)."
}
Write-Host "Prepared WebView2 Runtime $runtimeVersion at $cachePath"
$extractedRuntime = Join-Path $extractDirectory $runtimeFolderName
$runtimeExecutable = Join-Path $extractedRuntime "msedgewebview2.exe"
if (!(Test-Path $runtimeExecutable)) {
throw "Extracted WebView2 runtime is missing msedgewebview2.exe."
}
if (Test-Path $RuntimeDirectory) {
Remove-Item -LiteralPath $RuntimeDirectory -Recurse -Force
}
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $RuntimeDirectory) | Out-Null
Move-Item -LiteralPath $extractedRuntime -Destination $RuntimeDirectory
Write-Host "Prepared WebView2 fixed runtime $runtimeVersion at $RuntimeDirectory"

View File

@ -101,6 +101,7 @@ jobs:
timeout-minutes: 90
env:
CARGO_INCREMENTAL: "0"
RUSTFLAGS: -C debuginfo=line-tables-only -C target-feature=+crt-static
steps:
- uses: actions/checkout@v5
@ -118,36 +119,73 @@ jobs:
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Setup Rust for standard Windows
uses: dtolnay/rust-toolchain@1.97.1
- name: Check standard Windows dependency path
run: cargo check --locked --package dbx --no-default-features --target x86_64-pc-windows-msvc
- name: Setup Rust for Windows 7
uses: dtolnay/rust-toolchain@nightly
with:
toolchain: nightly-2026-07-22
components: rust-src
- name: Prepare Win7-compatible WebView2 loader
shell: pwsh
run: ./.github/scripts/prepare-webview2-win7-loader.ps1
- name: Prepare WebView2 109 fixed runtime
shell: pwsh
run: ./.github/scripts/prepare-webview2-win7-runtime.ps1
- name: Probe WebView2 109 fixed runtime
shell: pwsh
run: ./.github/scripts/assert-webview2-win7-runtime.ps1
- name: Build frontend
run: pnpm build
- name: Build DBX for Windows 7
run: cargo build --locked --package dbx --release --target x86_64-win7-windows-msvc -Z build-std=std,panic_abort
- name: Prepare WebView2 109 offline runtime
shell: pwsh
run: ./.github/scripts/prepare-webview2-win7-runtime.ps1
run: |
$env:TAURI_CONFIG = Get-Content src-tauri/tauri.webview2-win7-fixed.conf.json -Raw
cargo build --locked --package dbx --release --features custom-protocol --target x86_64-win7-windows-msvc -Z build-std=std,panic_abort
- name: Bundle Windows 7 offline installer
- name: Audit Windows 7 PE imports
shell: pwsh
run: ./.github/scripts/assert-win7-pe-compat.ps1 -BinaryPath target/x86_64-win7-windows-msvc/release/dbx.exe
- name: Bundle Windows 7 fixed-runtime installer
shell: pwsh
run: |
$bundleDir = "target/x86_64-win7-windows-msvc/release/bundle/nsis"
pnpm tauri bundle --bundles nsis --target x86_64-win7-windows-msvc --config src-tauri/tauri.webview2-win7-offline.conf.json
pnpm tauri bundle --bundles nsis --target x86_64-win7-windows-msvc --config src-tauri/tauri.webview2-win7-fixed.conf.json
$installer = Get-ChildItem $bundleDir -Filter "*.exe" |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
if (!$installer) {
Write-Error "Missing Windows 7 WebView2 offline installer in ${bundleDir}"
Write-Error "Missing Windows 7 fixed-runtime installer in ${bundleDir}"
exit 1
}
Get-FileHash -LiteralPath $installer.FullName -Algorithm SHA256
- name: Audit Windows 7 installer contents
shell: pwsh
run: |
$installer = Get-ChildItem "target/x86_64-win7-windows-msvc/release/bundle/nsis" -Filter "*.exe" |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
./.github/scripts/assert-win7-installer-content.ps1 -InstallerPath $installer.FullName
- name: Upload Windows 7 test installer
uses: actions/upload-artifact@v4
with:
name: DBX-win7-fixed-runtime-test
path: target/x86_64-win7-windows-msvc/release/bundle/nsis/*.exe
if-no-files-found: error
retention-days: 7
duckdb-windows-driver:
needs: changes
if: needs.changes.outputs.duckdb_windows == 'true'
@ -453,6 +491,7 @@ jobs:
rust:
- 'crates/**'
- 'src-tauri/**'
- 'vendor/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain*'
@ -488,16 +527,27 @@ jobs:
- '.github/workflows/ci.yml'
- '.github/workflows/update-nix-pnpm-hash.yml'
windows_win7_bundle:
- '.github/scripts/assert-win7-pe-compat.ps1'
- '.github/scripts/assert-win7-installer-content.ps1'
- '.github/scripts/assert-webview2-win7-runtime.ps1'
- '.github/scripts/prepare-webview2-win7-loader.ps1'
- '.github/scripts/prepare-webview2-win7-runtime.ps1'
- '.github/workflows/ci.yml'
- '.github/workflows/release.yml'
- 'src-tauri/tauri.webview2-win7-offline.conf.json'
- 'src-tauri/tauri.webview2-win7-fixed.conf.json'
- 'src-tauri/build.rs'
- 'src-tauri/Cargo.toml'
- 'src-tauri/windows/nsis/**'
- 'src-tauri/src/commands/update.rs'
- 'crates/dbx-core/Cargo.toml'
- 'crates/dbx-core/src/db/postgres.rs'
- 'crates/dbx-core/src/update.rs'
- 'Cargo.toml'
- 'Cargo.lock'
- 'vendor/ctor/**'
- 'vendor/dirs-sys/**'
- 'vendor/pageant/**'
- 'vendor/wry/**'
github_scripts:
- '.github/scripts/**'
- '.github/workflows/ci.yml'
@ -522,13 +572,13 @@ jobs:
while IFS= read -r file; do
case "$file" in
Cargo.toml|Cargo.lock|rust-toolchain*|.github/workflows/ci.yml|*/Cargo.toml)
Cargo.toml|Cargo.lock|rust-toolchain*|.github/workflows/ci.yml|*/Cargo.toml|vendor/*)
full=true
break
;;
esac
done < <(git diff --name-only "$BASE_SHA" HEAD -- Cargo.toml Cargo.lock 'rust-toolchain*' crates src-tauri .github/workflows/ci.yml)
done < <(git diff --name-only "$BASE_SHA" HEAD -- Cargo.toml Cargo.lock 'rust-toolchain*' crates src-tauri vendor .github/workflows/ci.yml)
fi
echo "full=$full" >> "$GITHUB_OUTPUT"

View File

@ -357,6 +357,7 @@ jobs:
CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: ${{ secrets.SCCACHE_S3_BUCKET == '' && 'true' || 'false' }}
RUSTFLAGS: -C debuginfo=line-tables-only -C target-feature=+crt-static
steps:
- uses: actions/checkout@v5
@ -377,6 +378,18 @@ jobs:
toolchain: nightly-2026-07-22
components: rust-src
- name: Prepare Win7-compatible WebView2 loader
shell: pwsh
run: ./.github/scripts/prepare-webview2-win7-loader.ps1
- name: Prepare WebView2 109 fixed runtime
shell: pwsh
run: ./.github/scripts/prepare-webview2-win7-runtime.ps1
- name: Probe WebView2 109 fixed runtime
shell: pwsh
run: ./.github/scripts/assert-webview2-win7-runtime.ps1
- name: Setup sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
with:
@ -416,13 +429,16 @@ jobs:
run: pnpm build
- name: Build DBX for Windows 7
run: cargo build --locked --package dbx --release --target x86_64-win7-windows-msvc -Z build-std=std,panic_abort
- name: Prepare WebView2 109 offline runtime
shell: pwsh
run: ./.github/scripts/prepare-webview2-win7-runtime.ps1
run: |
$env:TAURI_CONFIG = Get-Content src-tauri/tauri.webview2-win7-fixed.conf.json -Raw
cargo build --locked --package dbx --release --features custom-protocol --target x86_64-win7-windows-msvc -Z build-std=std,panic_abort
- name: Bundle and upload Windows 7 offline installer
- name: Audit Windows 7 PE imports
shell: pwsh
run: ./.github/scripts/assert-win7-pe-compat.ps1 -BinaryPath target/x86_64-win7-windows-msvc/release/dbx.exe
- name: Bundle and upload Windows 7 fixed-runtime installer
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -438,16 +454,18 @@ jobs:
exit 1
}
pnpm tauri bundle --bundles nsis --target x86_64-win7-windows-msvc --config src-tauri/tauri.webview2-win7-offline.conf.json
pnpm tauri bundle --bundles nsis --target x86_64-win7-windows-msvc --config src-tauri/tauri.webview2-win7-fixed.conf.json
$installer = Get-ChildItem $bundleDir -Filter "*.exe" |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
if (!$installer) {
Write-Error "Missing Windows 7 WebView2 offline installer in ${bundleDir}"
Write-Error "Missing Windows 7 fixed-runtime installer in ${bundleDir}"
exit 1
}
./.github/scripts/assert-win7-installer-content.ps1 -InstallerPath $installer.FullName
Copy-Item $installer.FullName $offlineName -Force
gh release upload "${env:GITHUB_REF_NAME}" $offlineName --repo "${env:GITHUB_REPOSITORY}" --clobber

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ CLAUDE.md
dist/
coverage/
/target/
src-tauri/webview2-fixed-runtime/
plugins/jdbc/target/
plugins/jdbc/dependency-reduced-pom.xml
plugins/jdbc/lib/*.jar

8
Cargo.lock generated
View File

@ -2028,8 +2028,6 @@ dependencies = [
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
@ -5264,8 +5262,6 @@ dependencies = [
[[package]]
name = "pageant"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5"
dependencies = [
"base16ct",
"byteorder",
@ -5277,8 +5273,8 @@ dependencies = [
"sha2 0.11.0",
"thiserror 2.0.19",
"tokio",
"windows 0.61.3",
"windows 0.62.2",
"windows-strings 0.5.1",
]
[[package]]
@ -10370,8 +10366,6 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "wry"
version = "0.55.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
dependencies = [
"base64 0.22.1",
"block2",

View File

@ -1,7 +1,7 @@
[workspace]
resolver = "2"
members = ["src-tauri", "crates/dbx-core", "crates/dbx-web", "crates/dbx-mcp", "crates/dbx-cli"]
exclude = ["vendor/ctor", "vendor/rumqttc"]
exclude = ["vendor/ctor", "vendor/dirs-sys", "vendor/pageant", "vendor/rumqttc", "vendor/wry"]
[patch.crates-io]
# Tauri 2.11 uses ctor 0.8, which excludes Rust's win7 vendor. This vendors
@ -9,6 +9,14 @@ exclude = ["vendor/ctor", "vendor/rumqttc"]
ctor = { path = "vendor/ctor" }
# rumqttc 0.24 hardcodes MQTT 3.1.1; this patch adds MQTT 3.1 CONNECT encoding.
rumqttc = { path = "vendor/rumqttc" }
# Keep two small upstream crates on Win7-compatible API calls. Their current
# Windows bindings otherwise import COMBASE or WinRT APIs unavailable on Win7.
dirs-sys = { path = "vendor/dirs-sys" }
pageant = { path = "vendor/pageant" }
# Wry 0.55 probes and creates WebView2 with a null browser folder. Pass the
# bundled Fixed Runtime path explicitly so Windows 7 does not fall back to an
# unavailable system Runtime.
wry = { path = "vendor/wry" }
tokio-postgres = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "115f9fef10f0fc3669b5337955e4eb461fc349a6" }
postgres-types = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "115f9fef10f0fc3669b5337955e4eb461fc349a6" }
postgres-protocol = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "115f9fef10f0fc3669b5337955e4eb461fc349a6" }

View File

@ -4,6 +4,17 @@ import { describe, expect, it } from "vitest";
import { shouldAbortWindowsWebView2RuntimeFallback } from "@/lib/app/windowsWebView2RuntimePolicy";
const template = readFileSync(resolve(process.cwd(), "src-tauri/windows/nsis/installer.nsi"), "utf8");
const win7Config = JSON.parse(readFileSync(resolve(process.cwd(), "src-tauri/tauri.webview2-win7-fixed.conf.json"), "utf8"));
const win7RuntimeScript = readFileSync(resolve(process.cwd(), ".github/scripts/prepare-webview2-win7-runtime.ps1"), "utf8");
const win7PeAuditScript = readFileSync(resolve(process.cwd(), ".github/scripts/assert-win7-pe-compat.ps1"), "utf8");
const win7RuntimeProbeScript = readFileSync(resolve(process.cwd(), ".github/scripts/assert-webview2-win7-runtime.ps1"), "utf8");
const win7InstallerAuditScript = readFileSync(resolve(process.cwd(), ".github/scripts/assert-win7-installer-content.ps1"), "utf8");
const appCargoToml = readFileSync(resolve(process.cwd(), "src-tauri/Cargo.toml"), "utf8");
const workspaceCargoToml = readFileSync(resolve(process.cwd(), "Cargo.toml"), "utf8");
const appBuildScript = readFileSync(resolve(process.cwd(), "src-tauri/build.rs"), "utf8");
const wryWebView2Source = readFileSync(resolve(process.cwd(), "vendor/wry/src/webview2/mod.rs"), "utf8");
const ciWorkflow = readFileSync(resolve(process.cwd(), ".github/workflows/ci.yml"), "utf8");
const releaseWorkflow = readFileSync(resolve(process.cwd(), ".github/workflows/release.yml"), "utf8");
describe("Windows offline installer template", () => {
it.each([
@ -63,3 +74,72 @@ describe("Windows offline installer template", () => {
expect(template).toContain('ReadRegStr ${RESULT} HKCU "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\${WEBVIEW2APPGUID}" "pv"');
});
});
describe("Windows 7 fixed WebView2 runtime bundle", () => {
it("bundles a fixed runtime instead of invoking a system Runtime installer", () => {
expect(win7Config.bundle.windows.webviewInstallMode).toEqual({
type: "fixedRuntime",
path: "webview2-fixed-runtime",
});
expect(win7RuntimeScript).toContain("Microsoft.WebView2.FixedVersionRuntime.$runtimeVersion.x64");
expect(win7RuntimeScript).toContain("msedgewebview2.exe");
expect(win7RuntimeScript).not.toContain("microsoftedgestandaloneinstaller");
expect(win7RuntimeScript).not.toContain("go.microsoft.com/fwlink");
});
it("pins the archived Microsoft runtime before extracting it", () => {
expect(win7RuntimeScript).toContain('$runtimeVersion = "109.0.1518.78"');
expect(win7RuntimeScript).toContain('$runtimeSha256 = "7622281cf83de1a35e3a471f432f7a897d65f0a7d3975df08512b7b253dd45c7"');
expect(win7RuntimeScript).toContain("Get-FileHash -LiteralPath $archivePath -Algorithm SHA256");
expect(win7RuntimeScript).toContain("Get-AuthenticodeSignature -LiteralPath $archivePath");
expect(win7RuntimeScript).toContain('$signature.SignerCertificate.Subject -notmatch "Microsoft Corporation"');
expect(win7RuntimeScript).toContain('& $expand $archivePath "-F:*" $extractDirectory');
});
it("rejects executable imports that cannot load on Windows 7", () => {
expect(win7PeAuditScript).toContain('"combase.dll"');
expect(win7PeAuditScript).toContain('"api-ms-win-core-winrt-"');
expect(win7PeAuditScript).toContain('"CoIncrementMTAUsage"');
expect(win7PeAuditScript).toContain('"GetSystemTimePreciseAsFileTime"');
expect(win7PeAuditScript).toContain('"VCRUNTIME140.dll"');
expect(win7PeAuditScript).toContain('"MSVCP140.dll"');
expect(win7PeAuditScript).toContain('"ucrtbase.dll"');
expect(win7PeAuditScript).toContain('"api-ms-win-crt-"');
expect(win7PeAuditScript).toContain("dumpbin.exe");
});
it("probes the fixed runtime through the Win7-compatible loader", () => {
expect(win7RuntimeProbeScript).toContain("GetAvailableCoreWebView2BrowserVersionString");
expect(win7RuntimeProbeScript).toContain('$ExpectedVersion = "109.0.1518.78"');
expect(win7RuntimeProbeScript).toContain("msedgewebview2.exe");
expect(ciWorkflow).toContain("./.github/scripts/assert-webview2-win7-runtime.ps1");
expect(releaseWorkflow).toContain("./.github/scripts/assert-webview2-win7-runtime.ps1");
});
it("passes the configured fixed-runtime folder to WebView2 discovery and creation", () => {
expect(workspaceCargoToml).toContain('wry = { path = "vendor/wry" }');
expect(wryWebView2Source).toContain('std::env::var_os("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER")');
expect(wryWebView2Source).toContain(`CreateCoreWebView2EnvironmentWithOptions(
browser_executable_folder_ptr,`);
expect(wryWebView2Source).toContain("GetAvailableCoreWebView2BrowserVersionString(browser_executable_folder_ptr, &mut versioninfo)");
});
it("audits the files produced by the silent Win7 installer", () => {
expect(win7InstallerAuditScript).toContain('"webview2-fixed-runtime\\msedgewebview2.exe"');
expect(win7InstallerAuditScript).toContain('"dbx.exe"');
expect(ciWorkflow).toContain("./.github/scripts/assert-win7-installer-content.ps1");
expect(releaseWorkflow).toContain("./.github/scripts/assert-win7-installer-content.ps1");
});
it("builds the Windows 7 executable with the production custom protocol", () => {
expect(appCargoToml).toContain('custom-protocol = ["tauri/custom-protocol"]');
expect(appBuildScript).toContain("CARGO_FEATURE_CUSTOM_PROTOCOL");
expect(appBuildScript).toContain("CARGO_CFG_TARGET_VENDOR");
expect(ciWorkflow).toContain("--release --features custom-protocol --target x86_64-win7-windows-msvc");
expect(releaseWorkflow).toContain("--release --features custom-protocol --target x86_64-win7-windows-msvc");
expect(ciWorkflow).toContain("TAURI_CONFIG = Get-Content src-tauri/tauri.webview2-win7-fixed.conf.json -Raw");
expect(releaseWorkflow).toContain("TAURI_CONFIG = Get-Content src-tauri/tauri.webview2-win7-fixed.conf.json -Raw");
expect(ciWorkflow).toContain("target-feature=+crt-static");
expect(releaseWorkflow).toContain("target-feature=+crt-static");
});
});

View File

@ -49,7 +49,6 @@ tiberius = { version = "0.12.3", default-features = false, features = ["tds73",
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls", "socks"] }
prometheus-parse = "=0.2.5"
futures = "0.3"
iana-time-zone = "0.1"
mongodb = "3.2.5"
rumqttc = { version = "0.24", features = ["websocket"], optional = true }
russh = "0.60"
@ -80,3 +79,6 @@ notify = { version = "7", default-features = false, features = ["macos_kqueue"]
arc-swap = "1"
insta = { version = "1", features = ["json", "glob"] }
tempfile = "3"
[target.'cfg(not(all(windows, target_vendor = "win7")))'.dependencies]
iana-time-zone = "0.1"

View File

@ -1441,11 +1441,27 @@ async fn stream_query_rows_text_on_client(
}
pub async fn connect(url: &str, fallback_timeout: Duration) -> Result<Pool, String> {
let timezone = iana_time_zone::get_timezone().unwrap_or_else(|_| "UTC".to_string());
connect_with_local_timezone(url, fallback_timeout, &timezone).await
#[cfg(all(windows, target_vendor = "win7"))]
{
connect_with_optional_local_timezone(url, fallback_timeout, None).await
}
#[cfg(not(all(windows, target_vendor = "win7")))]
{
let timezone = iana_time_zone::get_timezone().unwrap_or_else(|_| "UTC".to_string());
connect_with_local_timezone(url, fallback_timeout, &timezone).await
}
}
async fn connect_with_local_timezone(url: &str, fallback_timeout: Duration, timezone: &str) -> Result<Pool, String> {
connect_with_optional_local_timezone(url, fallback_timeout, Some(timezone)).await
}
async fn connect_with_optional_local_timezone(
url: &str,
fallback_timeout: Duration,
timezone: Option<&str>,
) -> Result<Pool, String> {
let url_with_keepalive = inject_postgres_keepalive_params(url);
let postgres_url = postgres_connection_url(&url_with_keepalive)?;
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
@ -1488,7 +1504,9 @@ async fn connect_with_local_timezone(url: &str, fallback_timeout: Duration, time
let client =
pool.get().await.map_err(|e| format!("PostgreSQL connection failed: {}", pg_pool_error_to_string(e)))?;
if !pg_url_has_timezone_setting(url) {
set_automatic_postgres_timezone(&client, timezone).await?;
if let Some(timezone) = timezone {
set_automatic_postgres_timezone(&client, timezone).await?;
}
}
Ok(pool)

View File

@ -14,6 +14,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
[features]
default = ["duckdb-sidecar", "mq-admin", "sqlite-sqlcipher", "system-fonts"]
custom-protocol = ["tauri/custom-protocol"]
duckdb-sidecar = ["dbx-core/duckdb-sidecar"]
mq-admin = ["dbx-core/mq-admin"]
sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"]

View File

@ -1,4 +1,9 @@
fn main() {
let is_win7_target = std::env::var("CARGO_CFG_TARGET_VENDOR").as_deref() == Ok("win7");
if is_win7_target && std::env::var_os("CARGO_FEATURE_CUSTOM_PROTOCOL").is_none() {
panic!("Windows 7 release builds must enable the custom-protocol feature");
}
// Force rebuild to re-embed frontend assets
tauri_build::build()
}

View File

@ -3,8 +3,8 @@
"createUpdaterArtifacts": false,
"windows": {
"webviewInstallMode": {
"silent": true,
"type": "offlineInstaller"
"type": "fixedRuntime",
"path": "webview2-fixed-runtime"
}
}
}

56
vendor/dirs-sys/Cargo.toml vendored Normal file
View File

@ -0,0 +1,56 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
name = "dirs-sys"
version = "0.5.0"
authors = ["Simon Ochsenreither <simon@ochsenreither.de>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "System-level helper functions for the dirs and directories crates."
readme = "README.md"
license = "MIT OR Apache-2.0"
repository = "https://github.com/dirs-dev/dirs-sys-rs"
[lib]
name = "dirs_sys"
path = "src/lib.rs"
[dependencies.option-ext]
version = "0.2.0"
[target.'cfg(target_os = "redox")'.dependencies.redox_users]
version = "0.5"
default-features = false
[target."cfg(unix)".dependencies.libc]
version = "0.2"
[target.'cfg(all(windows, target_vendor = "win7"))'.dependencies.windows-sys]
version = "=0.61.2"
features = [
"Win32_UI_Shell",
"Win32_Foundation",
"Win32_Globalization",
]
[target.'cfg(all(windows, not(target_vendor = "win7")))'.dependencies.windows-sys]
version = "=0.61.2"
features = [
"Win32_UI_Shell",
"Win32_Foundation",
"Win32_Globalization",
"Win32_System_Com",
]

6
vendor/dirs-sys/DBX-PATCH.md vendored Normal file
View File

@ -0,0 +1,6 @@
# DBX Windows 7 compatibility patch
This is `dirs-sys` 0.5.0 with `CoTaskMemFree` linked directly from
`ole32.dll` only for the Win7 target. `windows-sys` 0.61 links that function
from `combase.dll`, which is unavailable on Windows 7. Other Windows targets
retain the upstream `windows-sys` implementation and dependency features.

174
vendor/dirs-sys/LICENSE-APACHE vendored Normal file
View File

@ -0,0 +1,174 @@
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.

19
vendor/dirs-sys/LICENSE-MIT vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2018-2019 dirs-rs contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

49
vendor/dirs-sys/README.md vendored Normal file
View File

@ -0,0 +1,49 @@
[![crates.io](https://img.shields.io/crates/v/dirs-sys.svg?style=for-the-badge)](https://crates.io/crates/dirs-sys)
[![API documentation](https://img.shields.io/docsrs/dirs-sys/latest?style=for-the-badge)](https://docs.rs/dirs-sys/)
![as-is](https://img.shields.io/badge/maintenance-as--is-yellow.svg?style=for-the-badge)
# `dirs-sys`
System-level helper functions for the [`dirs`](https://github.com/dirs-dev/dirs-rs)
and [`directories`](https://github.com/dirs-dev/directories-rs) crates.
_Do not use this library directly, use [`dirs`](https://github.com/dirs-dev/dirs-rs)
or [`directories`](https://github.com/dirs-dev/directories-rs)._
## Compatibility
This crate only exists to facilitate code sharing between [`dirs`](https://github.com/dirs-dev/dirs-rs)
and [`directories`](https://github.com/dirs-dev/directories-rs).
There are no compatibility guarantees whatsoever.
Functions may change or disappear without warning or any kind of deprecation period.
## Platforms
This library is written in Rust, and supports Linux, Redox, macOS and Windows.
Other platforms are also supported; they use the Linux conventions.
## Build
It's possible to cross-compile this library if the necessary toolchains are installed with rustup.
This is helpful to ensure a change has not broken compilation on a different platform.
The following commands will build this library on Linux, macOS and Windows:
```
cargo build --target=x86_64-unknown-linux-gnu
cargo build --target=x86_64-pc-windows-gnu
cargo build --target=x86_64-apple-darwin
cargo build --target=x86_64-unknown-redox
```
## License
Licensed under either of
* Apache License, Version 2.0
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.

233
vendor/dirs-sys/src/lib.rs vendored Normal file
View File

@ -0,0 +1,233 @@
extern crate option_ext;
use std::ffi::OsString;
use std::path::PathBuf;
// we don't need to explicitly handle empty strings in the code above,
// because an empty string is not considered to be a absolute path here.
pub fn is_absolute_path(path: OsString) -> Option<PathBuf> {
let path = PathBuf::from(path);
if path.is_absolute() {
Some(path)
} else {
None
}
}
#[cfg(all(unix, not(target_os = "redox")))]
extern crate libc;
#[cfg(all(unix, not(target_os = "redox")))]
mod target_unix_not_redox {
use std::env;
use std::ffi::{CStr, OsString};
use std::mem;
use std::os::unix::ffi::OsStringExt;
use std::path::PathBuf;
use std::ptr;
use super::libc;
// https://github.com/rust-lang/rust/blob/2682b88c526d493edeb2d3f2df358f44db69b73f/library/std/src/sys/unix/os.rs#L595
pub fn home_dir() -> Option<PathBuf> {
return env::var_os("HOME")
.and_then(|h| if h.is_empty() { None } else { Some(h) })
.or_else(|| unsafe { fallback() })
.map(PathBuf::from);
#[cfg(any(target_os = "android", target_os = "ios", target_os = "emscripten"))]
unsafe fn fallback() -> Option<OsString> {
None
}
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "emscripten")))]
unsafe fn fallback() -> Option<OsString> {
let amt = match libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) {
n if n < 0 => 512 as usize,
n => n as usize,
};
let mut buf = Vec::with_capacity(amt);
let mut passwd: libc::passwd = mem::zeroed();
let mut result = ptr::null_mut();
match libc::getpwuid_r(
libc::getuid(),
&mut passwd,
buf.as_mut_ptr(),
buf.capacity(),
&mut result,
) {
0 if !result.is_null() => {
let ptr = passwd.pw_dir as *const _;
let bytes = CStr::from_ptr(ptr).to_bytes();
if bytes.is_empty() {
None
} else {
Some(OsStringExt::from_vec(bytes.to_vec()))
}
}
_ => None,
}
}
}
}
#[cfg(all(unix, not(target_os = "redox")))]
pub use self::target_unix_not_redox::home_dir;
#[cfg(target_os = "redox")]
extern crate redox_users;
#[cfg(target_os = "redox")]
mod target_redox {
use std::path::PathBuf;
use super::redox_users::{All, AllUsers, Config};
pub fn home_dir() -> Option<PathBuf> {
let current_uid = redox_users::get_uid().ok()?;
let users = AllUsers::basic(Config::default()).ok()?;
let user = users.get_by_id(current_uid)?;
Some(PathBuf::from(user.home.clone()))
}
}
#[cfg(target_os = "redox")]
pub use self::target_redox::home_dir;
#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
mod xdg_user_dirs;
#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
mod target_unix_not_mac {
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use super::{home_dir, is_absolute_path};
use super::xdg_user_dirs;
fn user_dir_file(home_dir: &Path) -> PathBuf {
env::var_os("XDG_CONFIG_HOME").and_then(is_absolute_path).unwrap_or_else(|| home_dir.join(".config")).join("user-dirs.dirs")
}
// this could be optimized further to not create a map and instead retrieve the requested path only
pub fn user_dir(user_dir_name: &str) -> Option<PathBuf> {
if let Some(home_dir) = home_dir() {
xdg_user_dirs::single(&home_dir, &user_dir_file(&home_dir), user_dir_name).remove(user_dir_name)
} else {
None
}
}
pub fn user_dirs(home_dir_path: &Path) -> HashMap<String, PathBuf> {
xdg_user_dirs::all(home_dir_path, &user_dir_file(home_dir_path))
}
}
#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]
pub use self::target_unix_not_mac::{user_dir, user_dirs};
#[cfg(target_os = "windows")]
extern crate windows_sys as windows;
#[cfg(target_os = "windows")]
mod target_windows {
use std::ffi::c_void;
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::slice;
use super::windows::Win32::UI::Shell;
#[cfg(target_vendor = "win7")]
#[link(name = "ole32")]
extern "system" {
fn CoTaskMemFree(pv: *const c_void);
}
pub fn known_folder(folder_id: windows::core::GUID) -> Option<PathBuf> {
unsafe {
let mut path_ptr: windows::core::PWSTR = std::ptr::null_mut();
let result = Shell::SHGetKnownFolderPath(
&folder_id,
0,
std::ptr::null_mut(),
&mut path_ptr
);
if result == 0 {
let len = windows::Win32::Globalization::lstrlenW(path_ptr) as usize;
let path = slice::from_raw_parts(path_ptr, len);
let ostr: OsString = OsStringExt::from_wide(path);
#[cfg(target_vendor = "win7")]
CoTaskMemFree(path_ptr as *const c_void);
#[cfg(not(target_vendor = "win7"))]
windows::Win32::System::Com::CoTaskMemFree(path_ptr as *const c_void);
Some(PathBuf::from(ostr))
} else {
#[cfg(target_vendor = "win7")]
CoTaskMemFree(path_ptr as *const c_void);
#[cfg(not(target_vendor = "win7"))]
windows::Win32::System::Com::CoTaskMemFree(path_ptr as *const c_void);
None
}
}
}
pub fn known_folder_profile() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_Profile)
}
pub fn known_folder_roaming_app_data() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_RoamingAppData)
}
pub fn known_folder_local_app_data() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_LocalAppData)
}
pub fn known_folder_music() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_Music)
}
pub fn known_folder_desktop() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_Desktop)
}
pub fn known_folder_documents() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_Documents)
}
pub fn known_folder_downloads() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_Downloads)
}
pub fn known_folder_pictures() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_Pictures)
}
pub fn known_folder_public() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_Public)
}
pub fn known_folder_templates() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_Templates)
}
pub fn known_folder_videos() -> Option<PathBuf> {
known_folder(Shell::FOLDERID_Videos)
}
}
#[cfg(target_os = "windows")]
pub use self::target_windows::{
known_folder, known_folder_profile, known_folder_roaming_app_data, known_folder_local_app_data,
known_folder_music, known_folder_desktop, known_folder_documents, known_folder_downloads,
known_folder_pictures, known_folder_public, known_folder_templates, known_folder_videos
};

248
vendor/dirs-sys/src/xdg_user_dirs.rs vendored Normal file
View File

@ -0,0 +1,248 @@
use std::collections::HashMap;
use std::ffi::OsString;
use std::fs;
use std::io::{self, Read};
use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use std::str;
use option_ext::OptionExt;
/// Returns all XDG user directories obtained from $(XDG_CONFIG_HOME)/user-dirs.dirs.
pub fn all(home_dir_path: &Path, user_dir_file_path: &Path) -> HashMap<String, PathBuf> {
let bytes = read_all(user_dir_file_path).unwrap_or(Vec::new());
parse_user_dirs(home_dir_path, None, &bytes)
}
/// Returns a single XDG user directory obtained from $(XDG_CONFIG_HOME)/user-dirs.dirs.
pub fn single(home_dir_path: &Path, user_dir_file_path: &Path, user_dir_name: &str) -> HashMap<String, PathBuf> {
let bytes = read_all(user_dir_file_path).unwrap_or(Vec::new());
parse_user_dirs(home_dir_path, Some(user_dir_name), &bytes)
}
fn parse_user_dirs(home_dir: &Path, user_dir: Option<&str>, bytes: &[u8]) -> HashMap<String, PathBuf> {
let mut user_dirs = HashMap::new();
for line in bytes.split(|b| *b == b'\n') {
let mut single_dir_found = false;
let (key, value) = match split_once(line, b'=') {
Some(kv) => kv,
None => continue,
};
let key = trim_blank(key);
let key = if key.starts_with(b"XDG_") && key.ends_with(b"_DIR") {
match str::from_utf8(&key[4..key.len()-4]) {
Ok(key) =>
if user_dir.contains(&key) {
single_dir_found = true;
key
} else if user_dir.is_none() {
key
} else {
continue
},
Err(_) => continue,
}
} else {
continue
};
// xdg-user-dirs-update uses double quotes and we don't support anything else.
let value = trim_blank(value);
let mut value = if value.starts_with(b"\"") && value.ends_with(b"\"") {
&value[1..value.len()-1]
} else {
continue
};
// Path should be either relative to the home directory or absolute.
let is_relative = if value == b"$HOME/" {
// "Note: To disable a directory, point it to the homedir."
// Source: https://www.freedesktop.org/wiki/Software/xdg-user-dirs/
// Additionally directory is reassigned to homedir when removed.
continue
} else if value.starts_with(b"$HOME/") {
value = &value[b"$HOME/".len()..];
true
} else if value.starts_with(b"/") {
false
} else {
continue
};
let value = OsString::from_vec(shell_unescape(value));
let path = if is_relative {
let mut path = PathBuf::from(&home_dir);
path.push(value);
path
} else {
PathBuf::from(value)
};
user_dirs.insert(key.to_owned(), path);
if single_dir_found {
break;
}
}
user_dirs
}
/// Reads the entire contents of a file into a byte vector.
fn read_all(path: &Path) -> io::Result<Vec<u8>> {
let mut file = fs::File::open(path)?;
let mut bytes = Vec::with_capacity(1024);
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
/// Returns bytes before and after first occurrence of separator.
fn split_once(bytes: &[u8], separator: u8) -> Option<(&[u8], &[u8])> {
bytes.iter().position(|b| *b == separator).map(|i| {
(&bytes[..i], &bytes[i+1..])
})
}
/// Returns a slice with leading and trailing <blank> characters removed.
fn trim_blank(bytes: &[u8]) -> &[u8] {
// Trim leading <blank> characters.
let i = bytes.iter().cloned().take_while(|b| *b == b' ' || *b == b'\t').count();
let bytes = &bytes[i..];
// Trim trailing <blank> characters.
let i = bytes.iter().cloned().rev().take_while(|b| *b == b' ' || *b == b'\t').count();
&bytes[..bytes.len()-i]
}
/// Unescape bytes escaped with POSIX shell double-quotes rules (as used by xdg-user-dirs-update).
fn shell_unescape(escaped: &[u8]) -> Vec<u8> {
// We assume that byte string was created by xdg-user-dirs-update which
// escapes all characters that might potentially have special meaning,
// so there is no need to check if backslash is actually followed by
// $ ` " \ or a <newline>.
let mut unescaped: Vec<u8> = Vec::with_capacity(escaped.len());
let mut i = escaped.iter().cloned();
while let Some(b) = i.next() {
if b == b'\\' {
if let Some(b) = i.next() {
unescaped.push(b);
}
} else {
unescaped.push(b);
}
}
unescaped
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use super::{parse_user_dirs, shell_unescape, split_once, trim_blank};
#[test]
fn test_trim_blank() {
assert_eq!(b"x", trim_blank(b"x"));
assert_eq!(b"", trim_blank(b" \t "));
assert_eq!(b"hello there", trim_blank(b" \t hello there \t "));
assert_eq!(b"\r\n", trim_blank(b"\r\n"));
}
#[test]
fn test_split_once() {
assert_eq!(None, split_once(b"a b c", b'='));
assert_eq!(Some((b"before".as_ref(), b"after".as_ref())), split_once(b"before=after", b'='));
}
#[test]
fn test_shell_unescape() {
assert_eq!(b"abc", shell_unescape(b"abc").as_slice());
assert_eq!(b"x\\y$z`", shell_unescape(b"x\\\\y\\$z\\`").as_slice());
}
#[test]
fn test_parse_empty() {
assert_eq!(HashMap::new(), parse_user_dirs(Path::new("/root/"), None, b""));
assert_eq!(HashMap::new(), parse_user_dirs(Path::new("/root/"), Some("MUSIC"), b""));
}
#[test]
fn test_absolute_path_is_accepted() {
let mut dirs = HashMap::new();
dirs.insert("MUSIC".to_owned(), PathBuf::from("/media/music"));
let bytes = br#"XDG_MUSIC_DIR="/media/music""#;
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), None, bytes));
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), Some("MUSIC"), bytes));
}
#[test]
fn test_relative_path_is_rejected() {
let dirs = HashMap::new();
let bytes = br#"XDG_MUSIC_DIR="music""#;
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), None, bytes));
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), Some("MUSIC"), bytes));
}
#[test]
fn test_relative_to_home() {
let mut dirs = HashMap::new();
dirs.insert("MUSIC".to_owned(), PathBuf::from("/home/john/Music"));
let bytes = br#"XDG_MUSIC_DIR="$HOME/Music""#;
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), None, bytes));
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), Some("MUSIC"), bytes));
}
#[test]
fn test_disabled_directory() {
let dirs = HashMap::new();
let bytes = br#"XDG_MUSIC_DIR="$HOME/""#;
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), None, bytes));
assert_eq!(dirs, parse_user_dirs(Path::new("/home/john"), Some("MUSIC"), bytes));
}
#[test]
fn test_parse_user_dirs() {
let mut dirs: HashMap<String, PathBuf> = HashMap::new();
dirs.insert("DESKTOP".to_string(), PathBuf::from("/home/bob/Desktop"));
dirs.insert("DOWNLOAD".to_string(), PathBuf::from("/home/bob/Downloads"));
dirs.insert("PICTURES".to_string(), PathBuf::from("/home/eve/pics"));
let bytes = br#"
# This file is written by xdg-user-dirs-update
# If you want to change or add directories, just edit the line you're
# interested in. All local changes will be retained on the next run.
# Format is XDG_xxx_DIR="$HOME/yyy", where yyy is a shell-escaped
# homedir-relative path, or XDG_xxx_DIR="/yyy", where /yyy is an
# absolute path. No other format is supported.
XDG_DESKTOP_DIR="$HOME/Desktop"
XDG_DOWNLOAD_DIR="$HOME/Downloads"
XDG_TEMPLATES_DIR=""
XDG_PUBLICSHARE_DIR="$HOME"
XDG_DOCUMENTS_DIR="$HOME/"
XDG_PICTURES_DIR="/home/eve/pics"
XDG_VIDEOS_DIR="$HOxyzME/Videos"
"#;
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), None, bytes));
let mut dirs: HashMap<String, PathBuf> = HashMap::new();
dirs.insert("DESKTOP".to_string(), PathBuf::from("/home/bob/Desktop"));
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("DESKTOP"), bytes));
let mut dirs: HashMap<String, PathBuf> = HashMap::new();
dirs.insert("PICTURES".to_string(), PathBuf::from("/home/eve/pics"));
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("PICTURES"), bytes));
let dirs: HashMap<String, PathBuf> = HashMap::new();
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("TEMPLATES"), bytes));
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("PUBLICSHARE"), bytes));
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("DOCUMENTS"), bytes));
assert_eq!(dirs, parse_user_dirs(Path::new("/home/bob"), Some("VIDEOS"), bytes));
}
}

108
vendor/pageant/Cargo.toml vendored Normal file
View File

@ -0,0 +1,108 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
rust-version = "1.85"
name = "pageant"
version = "0.2.1"
authors = ["Eugene <inbox@null.page>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Pageant SSH agent transport client."
documentation = "https://docs.rs/pageant"
readme = false
license = "Apache-2.0"
repository = "https://github.com/warp-tech/russh"
resolver = "2"
[features]
default = [
"wmmessage",
"namedpipes",
]
namedpipes = [
"tokio/net",
"tokio/time",
"dep:sha2",
]
wmmessage = [
"tokio/rt",
"tokio/io-util",
]
[lib]
name = "pageant"
path = "src/lib.rs"
[dependencies.thiserror]
version = "2.0.18"
[target."cfg(windows)".dependencies.base16ct]
version = "1"
features = ["alloc"]
[target."cfg(windows)".dependencies.byteorder]
version = "1.4"
[target."cfg(windows)".dependencies.bytes]
version = "1.7"
[target."cfg(windows)".dependencies.delegate]
version = "0.13"
[target."cfg(windows)".dependencies.futures]
version = "0.3"
[target."cfg(windows)".dependencies.log]
version = "0.4.11"
[target."cfg(windows)".dependencies.rand]
version = "0.10"
features = ["thread_rng"]
[target."cfg(windows)".dependencies.sha2]
version = "0.11"
features = ["oid"]
optional = true
[target."cfg(windows)".dependencies.tokio]
version = "1.17.0"
[target.'cfg(all(windows, target_vendor = "win7"))'.dependencies.windows-win7]
version = "=0.61.3"
features = [
"Win32_Security",
"Win32_Security_Authentication_Identity",
"Win32_Security_Cryptography",
"Win32_UI_WindowsAndMessaging",
"Win32_System_Memory",
"Win32_System_Threading",
"Win32_System_DataExchange",
]
package = "windows"
[target.'cfg(all(windows, not(target_vendor = "win7")))'.dependencies.windows-modern]
version = "0.62"
features = [
"Win32_Security",
"Win32_Security_Authentication_Identity",
"Win32_Security_Cryptography",
"Win32_UI_WindowsAndMessaging",
"Win32_System_Memory",
"Win32_System_Threading",
"Win32_System_DataExchange",
]
package = "windows"

6
vendor/pageant/DBX-PATCH.md vendored Normal file
View File

@ -0,0 +1,6 @@
# DBX Windows 7 compatibility patch
This is `pageant` 0.2.1 with WinRT `HSTRING` removed from Pageant window and
mapping names. It uses null-terminated UTF-16 strings instead. The Win7 target
uses Windows bindings 0.61.3 to avoid COMBASE and WinRT imports unavailable on
Windows 7, while other Windows targets retain the upstream 0.62 dependency.

44
vendor/pageant/src/error.rs vendored Normal file
View File

@ -0,0 +1,44 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Pageant not found")]
NotFound,
#[error("Buffer overflow")]
Overflow,
#[error("No response from Pageant")]
NoResponse,
#[error("Invalid Cookie")]
InvalidCookie,
#[error("NamedPipe keeps returning Busy")]
PipeBusy,
#[error("Invalid Username")]
InvalidUsername,
#[cfg(windows)]
#[error(transparent)]
WindowsError(#[from] crate::windows::core::Error),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error("Multiple Errors")]
Multiple(Vec<Self>),
}
impl Error {
#[cfg(windows)]
pub(crate) fn from_win32() -> Self {
#[cfg(target_vendor = "win7")]
let error = crate::windows::core::Error::from_win32();
#[cfg(not(target_vendor = "win7"))]
let error = crate::windows::core::Error::from_thread();
Self::WindowsError(error)
}
}

113
vendor/pageant/src/interface.rs vendored Normal file
View File

@ -0,0 +1,113 @@
use std::pin::Pin;
use log::debug;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::Error;
#[cfg(all(windows, feature = "namedpipes"))]
use crate::namedpipes;
#[cfg(all(windows, feature = "wmmessage"))]
use crate::wmmessage;
/// Pageant transport stream (using one of the available transport implementations).
/// Implements [AsyncRead] and [AsyncWrite].
pub enum PageantStream {
#[cfg(all(windows, feature = "wmmessage"))]
WmMessage(wmmessage::PageantStream),
#[cfg(all(windows, feature = "namedpipes"))]
NamedPipes(namedpipes::PageantStream),
}
impl PageantStream {
pub async fn new() -> Result<Self, Error> {
let mut errors = vec![];
// if compiled in, try the more modern named pipes approach first:
#[cfg(all(windows, feature = "namedpipes"))]
{
match namedpipes::PageantStream::new().await {
Ok(s) => {
return Ok(Self::NamedPipes(s));
}
Err(e) => {
debug!("Pageant NamedPipes connection failed: {e}");
errors.push(e);
}
}
}
#[cfg(all(windows, feature = "wmmessage"))]
{
match wmmessage::PageantStream::new().await {
Ok(s) => {
return Ok(Self::WmMessage(s));
}
Err(e) => {
debug!("Pageant WM_Message connection failed: {e}");
errors.push(e);
}
}
}
if errors.len() == 1
&& let Some(err) = errors.pop()
{
Err(err)
} else {
Err(Error::Multiple(errors))
}
}
}
impl AsyncRead for PageantStream {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut *self {
#[cfg(all(windows, feature = "wmmessage"))]
Self::WmMessage(i) => wmmessage::PageantStream::poll_read(Pin::new(i), cx, buf),
#[cfg(all(windows, feature = "namedpipes"))]
Self::NamedPipes(i) => namedpipes::PageantStream::poll_read(Pin::new(i), cx, buf),
}
}
}
impl AsyncWrite for PageantStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<Result<usize, std::io::Error>> {
match &mut *self {
#[cfg(all(windows, feature = "wmmessage"))]
Self::WmMessage(i) => wmmessage::PageantStream::poll_write(Pin::new(i), cx, buf),
#[cfg(all(windows, feature = "namedpipes"))]
Self::NamedPipes(i) => namedpipes::PageantStream::poll_write(Pin::new(i), cx, buf),
}
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
match &mut *self {
#[cfg(all(windows, feature = "wmmessage"))]
Self::WmMessage(i) => wmmessage::PageantStream::poll_flush(Pin::new(i), cx),
#[cfg(all(windows, feature = "namedpipes"))]
Self::NamedPipes(i) => namedpipes::PageantStream::poll_flush(Pin::new(i), cx),
}
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
match &mut *self {
#[cfg(all(windows, feature = "wmmessage"))]
Self::WmMessage(i) => wmmessage::PageantStream::poll_shutdown(Pin::new(i), cx),
#[cfg(all(windows, feature = "namedpipes"))]
Self::NamedPipes(i) => namedpipes::PageantStream::poll_shutdown(Pin::new(i), cx),
}
}
}

32
vendor/pageant/src/lib.rs vendored Normal file
View File

@ -0,0 +1,32 @@
//! # Pageant SSH agent transport protocol implementation
//!
//! This crate provides a [PageantStream] type that implements [AsyncRead] and [AsyncWrite] traits and can be used to talk to a running Pageant instance.
//!
//! This crate only implements the transport, not the actual SSH agent protocol.
#![deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
#[cfg(all(windows, target_vendor = "win7"))]
pub(crate) use windows_win7 as windows;
#[cfg(all(windows, not(target_vendor = "win7")))]
pub(crate) use windows_modern as windows;
mod error;
pub use error::*;
#[cfg(all(windows, feature = "wmmessage"))]
pub mod wmmessage;
#[cfg(all(windows, feature = "namedpipes"))]
pub mod namedpipes;
#[cfg(windows)]
mod interface;
#[cfg(windows)]
pub use interface::*;

154
vendor/pageant/src/namedpipes.rs vendored Normal file
View File

@ -0,0 +1,154 @@
use std::io::IoSlice;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use base16ct::lower;
use delegate::delegate;
use log::debug;
use sha2::{Digest, Sha256};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient};
use crate::windows::Win32::Foundation::ERROR_PIPE_BUSY;
use crate::windows::Win32::Security::Authentication::Identity::{GetUserNameExA, NameUserPrincipal};
use crate::windows::Win32::Security::Cryptography::{
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_CROSS_PROCESS, CryptProtectMemory,
};
use crate::windows::core::PSTR;
use crate::Error;
/// Pageant transport stream. Implements [AsyncRead] and [AsyncWrite].
pub struct PageantStream {
stream: NamedPipeClient,
}
impl PageantStream {
pub async fn new() -> Result<Self, Error> {
let pipe_name = Self::determine_pipe_name()?;
debug!("Opening pipe '{}'", pipe_name);
let mut timeout_counter = 0;
let stream = loop {
match ClientOptions::new().open(&pipe_name) {
Ok(client) => break client,
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (),
Err(e) => return Err(e.into()),
}
timeout_counter += 1;
if timeout_counter > 40 {
return Err(Error::PipeBusy);
}
tokio::time::sleep(Duration::from_millis(50)).await;
};
Ok(Self { stream })
}
fn determine_pipe_name() -> Result<String, Error> {
let username = Self::get_username()?;
let suffix = Self::capi_obfuscate_string("Pageant")?;
Ok(format!("\\\\.\\pipe\\pageant.{username}.{suffix}"))
}
fn get_username() -> Result<String, Error> {
unsafe {
let mut name_length = 0;
// don't check result on this, always returns ERROR_MORE_DATA
GetUserNameExA(NameUserPrincipal, None, &mut name_length);
let mut name_buf = vec![0u8; name_length as usize];
if !GetUserNameExA(
NameUserPrincipal,
Some(PSTR(name_buf.as_mut_ptr())),
&mut name_length,
) {
// Pageant falls back to GetUserNameA here,
// but as far as I can tell, all Versions of Windows supported by Rust today
// should be able to answer the UserNameEx request - the comments in Pageant source
// point to Windows XP and earlier compatibility...
return Err(Error::from_win32());
}
//remove terminating null
if let Some(0) = name_buf.pop() {
let mut name = String::from_utf8(name_buf).map_err(|_| Error::InvalidUsername)?;
if let Some(at_index) = name.find('@') {
name.drain(at_index..);
}
Ok(name)
} else {
Err(Error::InvalidUsername)
}
}
}
fn capi_obfuscate_string(input: &str) -> Result<String, Error> {
let mut cryptlen = input.len() + 1;
cryptlen = cryptlen.next_multiple_of(CRYPTPROTECTMEMORY_BLOCK_SIZE as usize);
let mut cryptdata = vec![0u8; cryptlen];
// copy cleartext into crypt buffer:
cryptdata
.iter_mut()
.zip(input.as_bytes())
.for_each(|(c, i)| *c = *i);
// (since the buffer is initialized to 0 and always at least 1 longer than the input,
// we don't need to worry about terminating the string)
unsafe {
// Errors are explicitly ignored:
let _ = CryptProtectMemory(
cryptdata.as_mut_ptr() as *mut _,
cryptlen as u32,
CRYPTPROTECTMEMORY_CROSS_PROCESS,
);
}
let mut hasher = Sha256::new();
hasher.update((cryptdata.len() as u32).to_be_bytes());
hasher.update(&cryptdata);
Ok(lower::encode_string(&hasher.finalize()))
}
}
impl AsyncRead for PageantStream {
delegate! {
to Pin::new(&mut self.stream) {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<Result<(), std::io::Error>>;
}
}
}
impl AsyncWrite for PageantStream {
delegate! {
to Pin::new(&mut self.stream) {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>>;
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;
fn poll_write_vectored(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[IoSlice<'_>],
) -> Poll<Result<usize, std::io::Error>>;
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;
}
to Pin::new(&self.stream) {
fn is_write_vectored(&self) -> bool;
}
}
}

291
vendor/pageant/src/wmmessage.rs vendored Normal file
View File

@ -0,0 +1,291 @@
use std::ffi::CString;
use std::io::IoSlice;
use std::pin::Pin;
use std::task::{Context, Poll};
use byteorder::{BigEndian, ByteOrder};
use bytes::BytesMut;
use delegate::delegate;
use log::debug;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf};
use crate::windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, INVALID_HANDLE_VALUE, LPARAM, WPARAM};
use crate::windows::Win32::Security::{
GetTokenInformation, InitializeSecurityDescriptor, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES,
SECURITY_DESCRIPTOR, SetSecurityDescriptorOwner, TOKEN_QUERY, TOKEN_USER, TokenUser,
};
use crate::windows::Win32::System::DataExchange::COPYDATASTRUCT;
use crate::windows::Win32::System::Memory::{
CreateFileMappingW, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, PAGE_READWRITE,
UnmapViewOfFile,
};
use crate::windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
use crate::windows::Win32::UI::WindowsAndMessaging::{FindWindowW, SendMessageA, WM_COPYDATA};
use crate::windows::core::PCWSTR;
use crate::Error;
/// Pageant transport stream. Implements [AsyncRead] and [AsyncWrite].
///
/// The stream has a unique cookie and requests made in the same stream are considered the same "session".
pub struct PageantStream {
stream: DuplexStream,
}
impl PageantStream {
pub async fn new() -> Result<Self, Error> {
let (one, mut two) = tokio::io::duplex(_AGENT_MAX_MSGLEN * 100);
let cookie = rand::random::<u64>().to_string();
tokio::spawn(async move {
let mut buf = BytesMut::new();
while let Ok(n) = two.read_buf(&mut buf).await {
if n == 0 {
break;
}
if buf.len() < 4 {
continue;
}
let len = BigEndian::read_u32(&buf) as usize;
if buf.len() < len + 4 {
continue;
}
let msg = buf.split_to(len + 4).freeze();
let Ok(response) = query_pageant_direct(cookie.clone(), &msg).map_err(|e| {
debug!("Pageant query failed: {:?}", e);
e
}) else {
break;
};
two.write_all(&response).await?
}
std::io::Result::Ok(())
});
Ok(Self { stream: one })
}
}
impl AsyncRead for PageantStream {
delegate! {
to Pin::new(&mut self.stream) {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<Result<(), std::io::Error>>;
}
}
}
impl AsyncWrite for PageantStream {
delegate! {
to Pin::new(&mut self.stream) {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>>;
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;
fn poll_write_vectored(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[IoSlice<'_>],
) -> Poll<Result<usize, std::io::Error>>;
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;
}
to Pin::new(&self.stream) {
fn is_write_vectored(&self) -> bool;
}
}
}
struct MemoryMap {
filemap: HANDLE,
view: MEMORY_MAPPED_VIEW_ADDRESS,
length: usize,
pos: usize,
}
impl MemoryMap {
fn new(
name: String,
length: usize,
security_attributes: Option<SECURITY_ATTRIBUTES>,
) -> Result<Self, Error> {
let wide_name = wide_null(&name);
let filemap = unsafe {
CreateFileMappingW(
INVALID_HANDLE_VALUE,
security_attributes.map(|sa| &sa as *const _),
PAGE_READWRITE,
0,
length as u32,
PCWSTR(wide_name.as_ptr()),
)
}?;
if filemap.is_invalid() {
return Err(Error::from_win32());
}
let view = unsafe { MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) };
Ok(Self {
filemap,
view,
length,
pos: 0,
})
}
fn seek(&mut self, pos: usize) {
self.pos = pos;
}
fn write(&mut self, data: &[u8]) -> Result<(), Error> {
if self.pos + data.len() > self.length {
return Err(Error::Overflow);
}
if data.is_empty() {
return Ok(());
}
unsafe {
#[allow(clippy::indexing_slicing)] // length checked
std::ptr::copy_nonoverlapping(
&data[0] as *const u8,
self.view.Value.add(self.pos) as *mut u8,
data.len(),
);
}
self.pos += data.len();
Ok(())
}
fn read(&mut self, n: usize) -> Vec<u8> {
let out = vec![0; n];
unsafe {
std::ptr::copy_nonoverlapping(
self.view.Value.add(self.pos) as *const u8,
out.as_ptr() as *mut u8,
n,
);
}
self.pos += n;
out
}
}
impl Drop for MemoryMap {
fn drop(&mut self) {
unsafe {
let _ = UnmapViewOfFile(self.view);
let _ = CloseHandle(self.filemap);
}
}
}
fn find_pageant_window() -> Result<HWND, Error> {
let pageant = wide_null("Pageant");
let w = unsafe { FindWindowW(PCWSTR(pageant.as_ptr()), PCWSTR(pageant.as_ptr())) }?;
if w.is_invalid() {
return Err(Error::NotFound);
}
Ok(w)
}
fn wide_null(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
const _AGENT_COPYDATA_ID: u64 = 0x804E50BA;
const _AGENT_MAX_MSGLEN: usize = 8192;
pub fn is_pageant_running() -> bool {
find_pageant_window().is_ok()
}
fn get_current_process_user() -> Result<TOKEN_USER, Error> {
unsafe {
let mut process_token = HANDLE::default();
OpenProcessToken(
GetCurrentProcess(),
TOKEN_QUERY,
&mut process_token as *mut _,
)?;
let mut info_size = 0;
let _ = GetTokenInformation(process_token, TokenUser, None, 0, &mut info_size);
let mut buffer = vec![0; info_size as usize];
GetTokenInformation(
process_token,
TokenUser,
Some(buffer.as_mut_ptr() as *mut _),
buffer.len() as u32,
&mut info_size,
)?;
let user: TOKEN_USER = *(buffer.as_ptr() as *const _);
let _ = CloseHandle(process_token);
Ok(user)
}
}
/// Send a one-off query to Pageant and return a response.
pub fn query_pageant_direct(cookie: String, msg: &[u8]) -> Result<Vec<u8>, Error> {
let hwnd = find_pageant_window()?;
let map_name = format!("PageantRequest{cookie}");
let user = get_current_process_user()?;
let mut sd = SECURITY_DESCRIPTOR::default();
let sa = SECURITY_ATTRIBUTES {
lpSecurityDescriptor: &mut sd as *mut _ as *mut _,
bInheritHandle: true.into(),
nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
};
let psd = PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _);
unsafe {
InitializeSecurityDescriptor(psd, 1)?;
SetSecurityDescriptorOwner(psd, Some(user.User.Sid), false)?;
}
let mut map: MemoryMap = MemoryMap::new(map_name.clone(), _AGENT_MAX_MSGLEN, Some(sa))?;
map.write(msg)?;
let char_buffer = CString::new(map_name.as_bytes()).map_err(|_| Error::InvalidCookie)?;
let cds = COPYDATASTRUCT {
dwData: _AGENT_COPYDATA_ID as usize,
cbData: char_buffer.as_bytes().len() as u32,
lpData: char_buffer.as_bytes().as_ptr() as *mut _,
};
let response = unsafe {
SendMessageA(
hwnd,
WM_COPYDATA,
WPARAM(0), // Should be window handle to requesting app, which we don't have
LPARAM(&cds as *const _ as isize),
)
};
if response.0 == 0 {
return Err(Error::NoResponse);
}
map.seek(0);
let mut buf = map.read(4);
if buf.len() < 4 {
return Err(Error::NoResponse);
}
#[allow(clippy::indexing_slicing)] // length checked
let size = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
buf.extend(map.read(size));
Ok(buf)
}

424
vendor/wry/Cargo.toml vendored Normal file
View File

@ -0,0 +1,424 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.77"
name = "wry"
version = "0.55.1"
authors = ["Tauri Programme within The Commons Conservancy"]
build = "build.rs"
exclude = [
"/.changes",
"/.github",
"/audits",
"/wry-logo.svg",
]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Cross-platform WebView rendering library"
documentation = "https://docs.rs/wry"
readme = "README.md"
categories = ["gui"]
license = "Apache-2.0 OR MIT"
repository = "https://github.com/tauri-apps/wry"
[package.metadata.docs.rs]
no-default-features = true
features = [
"protocol",
"os-webview",
]
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"x86_64-apple-darwin",
]
[features]
default = [
"protocol",
"os-webview",
"x11",
]
devtools = []
fullscreen = []
linux-body = [
"webkit2gtk/v2_40",
"os-webview",
]
mac-proxy = []
os-webview = [
"javascriptcore-rs",
"webkit2gtk",
"webkit2gtk-sys",
"dep:gtk",
"soup3",
]
protocol = []
serde = ["dpi/serde"]
tracing = ["dep:tracing"]
transparent = []
x11 = [
"x11-dl",
"gdkx11",
"tao/x11",
]
[lib]
name = "wry"
path = "src/lib.rs"
[[example]]
name = "async_custom_protocol"
path = "examples/async_custom_protocol.rs"
[[example]]
name = "cookies"
path = "examples/cookies.rs"
[[example]]
name = "custom_protocol"
path = "examples/custom_protocol.rs"
[[example]]
name = "custom_titlebar"
path = "examples/custom_titlebar.rs"
[[example]]
name = "gtk_multiwebview"
path = "examples/gtk_multiwebview.rs"
[[example]]
name = "gtk_opengl"
path = "examples/gtk_opengl.rs"
[[example]]
name = "multiwebview"
path = "examples/multiwebview.rs"
[[example]]
name = "multiwindow"
path = "examples/multiwindow.rs"
[[example]]
name = "reparent"
path = "examples/reparent.rs"
[[example]]
name = "simple"
path = "examples/simple.rs"
[[example]]
name = "streaming"
path = "examples/streaming.rs"
[[example]]
name = "transparent"
path = "examples/transparent.rs"
[[example]]
name = "wgpu"
path = "examples/wgpu.rs"
[[example]]
name = "window_border"
path = "examples/window_border.rs"
[[example]]
name = "winit"
path = "examples/winit.rs"
[dependencies.cookie]
version = "0.18"
[dependencies.dpi]
version = "0.1"
[dependencies.http]
version = "1.1"
[dependencies.once_cell]
version = "1"
[dependencies.raw-window-handle]
version = "0.6"
features = ["std"]
[dependencies.thiserror]
version = "2.0"
[dependencies.tracing]
version = "0.1"
optional = true
[dev-dependencies.base64]
version = "0.22"
[dev-dependencies.dom_query]
version = "0.27.0"
default-features = false
[dev-dependencies.getrandom]
version = "0.3"
[dev-dependencies.glow]
version = "0.16.0"
[dev-dependencies.http-range]
version = "0.1"
[dev-dependencies.libloading]
version = "0.8.9"
[dev-dependencies.percent-encoding]
version = "2.3"
[dev-dependencies.pollster]
version = "0.4.0"
[dev-dependencies.sha2]
version = "0.10"
[dev-dependencies.tao]
version = "0.35"
[dev-dependencies.wgpu]
version = "23"
[dev-dependencies.winit]
version = "0.30"
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.dirs]
version = "6"
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gdkx11]
version = "0.18"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gtk]
version = "0.18"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.javascriptcore-rs]
version = "=1.1.2"
features = ["v2_28"]
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.percent-encoding]
version = "2.3"
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.soup3]
version = "0.5"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.webkit2gtk]
version = "=2.0.2"
features = ["v2_38"]
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.webkit2gtk-sys]
version = "=2.0.2"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.x11-dl]
version = "2.21"
optional = true
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dev-dependencies.x11-dl]
version = "2.21"
[target.'cfg(target_os = "android")'.dependencies.base64]
version = "0.22"
[target.'cfg(target_os = "android")'.dependencies.crossbeam-channel]
version = "0.5"
[target.'cfg(target_os = "android")'.dependencies.dom_query]
version = "0.27.0"
default-features = false
[target.'cfg(target_os = "android")'.dependencies.jni]
version = "0.21"
[target.'cfg(target_os = "android")'.dependencies.libc]
version = "0.2"
[target.'cfg(target_os = "android")'.dependencies.ndk]
version = "0.9"
[target.'cfg(target_os = "android")'.dependencies.sha2]
version = "0.10"
[target.'cfg(target_os = "android")'.dependencies.tao-macros]
version = "0.1"
[target.'cfg(target_os = "ios")'.dependencies.objc2-ui-kit]
version = "0.3.0"
features = [
"std",
"objc2-core-foundation",
"UIResponder",
"UIScrollView",
"UIView",
"UIWindow",
"UIApplication",
"UIEvent",
"UIColor",
]
default-features = false
[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit]
version = "0.3.0"
features = [
"std",
"objc2-core-foundation",
"NSApplication",
"NSButton",
"NSControl",
"NSEvent",
"NSWindow",
"NSView",
"NSPasteboard",
"NSPanel",
"NSResponder",
"NSOpenPanel",
"NSSavePanel",
"NSMenu",
"NSGraphics",
"NSScreen",
]
default-features = false
[target.'cfg(target_os = "windows")'.dependencies.dunce]
version = "1"
[target.'cfg(target_os = "windows")'.dependencies.webview2-com]
version = "0.38"
[target.'cfg(target_os = "windows")'.dependencies.windows]
version = "0.61"
features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_LibraryLoader",
"Win32_System_Ole",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
"Win32_Globalization",
"Win32_UI_HiDpi",
"Win32_UI_Input",
"Win32_UI_Input_KeyboardAndMouse",
]
[target.'cfg(target_os = "windows")'.dependencies.windows-core]
version = "0.61"
[target.'cfg(target_os = "windows")'.dependencies.windows-version]
version = "0.1"
[target.'cfg(target_vendor = "apple")'.dependencies.block2]
version = "0.6"
[target.'cfg(target_vendor = "apple")'.dependencies.dirs]
version = "6"
[target.'cfg(target_vendor = "apple")'.dependencies.objc2]
version = "0.6.4"
features = [
"exception",
"disable-encoding-assertions",
]
[target.'cfg(target_vendor = "apple")'.dependencies.objc2-core-foundation]
version = "0.3.0"
features = [
"std",
"CFCGTypes",
]
default-features = false
[target.'cfg(target_vendor = "apple")'.dependencies.objc2-foundation]
version = "0.3.0"
features = [
"std",
"objc2-core-foundation",
"NSURLRequest",
"NSURL",
"NSString",
"NSKeyValueCoding",
"NSStream",
"NSDictionary",
"NSObject",
"NSData",
"NSEnumerator",
"NSKeyValueObserving",
"NSThread",
"NSJSONSerialization",
"NSDate",
"NSBundle",
"NSProcessInfo",
"NSValue",
"NSRange",
"NSRunLoop",
]
default-features = false
[target.'cfg(target_vendor = "apple")'.dependencies.objc2-web-kit]
version = "0.3.0"
features = [
"std",
"objc2-core-foundation",
"objc2-app-kit",
"block2",
"WKWebView",
"WKWebViewConfiguration",
"WKWebsiteDataStore",
"WKDownload",
"WKDownloadDelegate",
"WKNavigation",
"WKNavigationDelegate",
"WKUserContentController",
"WKURLSchemeHandler",
"WKPreferences",
"WKURLSchemeTask",
"WKScriptMessageHandler",
"WKUIDelegate",
"WKOpenPanelParameters",
"WKFrameInfo",
"WKSecurityOrigin",
"WKScriptMessage",
"WKNavigationAction",
"WKWebpagePreferences",
"WKNavigationResponse",
"WKUserScript",
"WKHTTPCookieStore",
"WKWindowFeatures",
]
default-features = false
[target.'cfg(target_vendor = "apple")'.dependencies.url]
version = "2.5"
[lints.rust.unexpected_cfgs]
level = "warn"
priority = 0
check-cfg = [
"cfg(linux)",
"cfg(gtk)",
]

9
vendor/wry/DBX-PATCH.md vendored Normal file
View File

@ -0,0 +1,9 @@
# DBX patch
DBX vendors Wry 0.55.1 to pass `WEBVIEW2_BROWSER_EXECUTABLE_FOLDER` directly
to both WebView2 Runtime discovery and environment creation on the Win7 target.
Upstream Wry passes a null browser folder to these APIs. That works with the
Evergreen Runtime but prevents DBX's Windows 7 build from reliably selecting
its bundled WebView2 109 Fixed Runtime. Other Windows targets retain the
upstream null-folder behavior.

201
vendor/wry/LICENSE-APACHE vendored Normal file
View File

@ -0,0 +1,201 @@
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.

21
vendor/wry/LICENSE-MIT vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2023 Ngo Iok Ui & Tauri Programme within The Commons Conservancy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

20
vendor/wry/LICENSE.spdx vendored Normal file
View File

@ -0,0 +1,20 @@
SPDXVersion: SPDX-2.1
DataLicense: CC0-1.0
PackageName: wry
DataFormat: SPDXRef-1
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
PackageHomePage: https://tauri.app
PackageLicenseDeclared: Apache-2.0
PackageLicenseDeclared: MIT
PackageCopyrightText: 2020-2023, The Tauri Programme in the Commons Conservancy
PackageSummary: <text>Wry is the official, rust-based webview
windowing service for Tauri.
</text>
PackageComment: <text>The package includes the following libraries; see
Relationship information.
</text>
Created: 2020-05-20T09:00:00Z
PackageDownloadLocation: git://github.com/tauri-apps/wry
PackageDownloadLocation: git+https://github.com/tauri-apps/wry.git
PackageDownloadLocation: git+ssh://github.com/tauri-apps/wry.git
Creator: Person: Daniel Thompson-Yvetot

317
vendor/wry/README.md vendored Normal file
View File

@ -0,0 +1,317 @@
<p align="center"><img height="100" src="https://raw.githubusercontent.com/tauri-apps/wry/refs/heads/dev/.github/splash.png" alt="WRY Webview Rendering library" /></p>
[![](https://img.shields.io/crates/v/wry?style=flat-square)](https://crates.io/crates/wry) [![](https://img.shields.io/docsrs/wry?style=flat-square)](https://docs.rs/wry/)
[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri)
[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S)
[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app)
[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation)
[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri)
Wry is a cross-platform WebView rendering library.
The webview requires a running event loop and a window type that implements [`HasWindowHandle`],
or a gtk container widget if you need to support X11 and Wayland.
You can use a windowing library like [`tao`] or [`winit`].
### Examples
This example leverages the [`HasWindowHandle`] and supports Windows, macOS, iOS, Android and Linux (X11 Only).
See the following example using [`winit`]:
```rust
#[derive(Default)]
struct App {
window: Option<Window>,
webview: Option<wry::WebView>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = event_loop.create_window(Window::default_attributes()).unwrap();
let webview = WebViewBuilder::new()
.with_url("https://tauri.app")
.build(&window)
.unwrap();
self.window = Some(window);
self.webview = Some(webview);
}
fn window_event(&mut self, _event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {}
}
let event_loop = EventLoop::new().unwrap();
let mut app = App::default();
event_loop.run_app(&mut app).unwrap();
```
If you also want to support Wayland too, then we recommend you use [`WebViewBuilderExtUnix::new_gtk`] on Linux.
See the following example using [`tao`]:
```rust
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let builder = WebViewBuilder::new().with_url("https://tauri.app");
#[cfg(not(target_os = "linux"))]
let webview = builder.build(&window).unwrap();
#[cfg(target_os = "linux")]
let webview = builder.build_gtk(window.gtk_window()).unwrap();
```
### Child webviews
You can use [`WebViewBuilder::build_as_child`] to create the webview as a child inside another window. This is supported on
macOS, Windows and Linux (X11 Only).
```rust
#[derive(Default)]
struct App {
window: Option<Window>,
webview: Option<wry::WebView>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = event_loop.create_window(Window::default_attributes()).unwrap();
let webview = WebViewBuilder::new()
.with_url("https://tauri.app")
.with_bounds(Rect {
position: LogicalPosition::new(100, 100).into(),
size: LogicalSize::new(200, 200).into(),
})
.build_as_child(&window)
.unwrap();
self.window = Some(window);
self.webview = Some(webview);
}
fn window_event(&mut self, _event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {}
}
let event_loop = EventLoop::new().unwrap();
let mut app = App::default();
event_loop.run_app(&mut app).unwrap();
```
If you want to support X11 and Wayland at the same time, we recommend using
[`WebViewExtUnix::new_gtk`] or [`WebViewBuilderExtUnix::new_gtk`] with [`gtk::Fixed`].
```rust
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let builder = WebViewBuilder::new()
.with_url("https://tauri.app")
.with_bounds(Rect {
position: LogicalPosition::new(100, 100).into(),
size: LogicalSize::new(200, 200).into(),
});
#[cfg(not(target_os = "linux"))]
let webview = builder.build_as_child(&window).unwrap();
#[cfg(target_os = "linux")]
let webview = {
# use gtk::prelude::*;
let vbox = window.default_vbox().unwrap(); // tao adds a gtk::Box by default
let fixed = gtk::Fixed::new();
fixed.show_all();
vbox.pack_start(&fixed, true, true, 0);
builder.build_gtk(&fixed).unwrap()
};
```
### Platform Considerations
Here is the underlying web engine each platform uses, and some dependencies you might need to install.
#### Linux
[WebKitGTK](https://webkitgtk.org/) is used to provide webviews on Linux which requires GTK,
so if the windowing library doesn't support GTK (as in [`winit`])
you'll need to call [`gtk::init`] before creating the webview and then call [`gtk::main_iteration_do`] alongside
your windowing library event loop.
```rust
#[derive(Default)]
struct App {
webview_window: Option<(Window, WebView)>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = event_loop.create_window(Window::default_attributes()).unwrap();
let webview = WebViewBuilder::new()
.with_url("https://tauri.app")
.build(&window)
.unwrap();
self.webview_window = Some((window, webview));
}
fn window_event(&mut self, _event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {}
// Advance GTK event loop <!----- IMPORTANT
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
#[cfg(target_os = "linux")]
while gtk::events_pending() {
gtk::main_iteration_do(false);
}
}
}
let event_loop = EventLoop::new().unwrap();
let mut app = App::default();
event_loop.run_app(&mut app).unwrap();
```
##### Linux Dependencies
###### Arch Linux / Manjaro:
```bash
sudo pacman -S webkit2gtk-4.1
```
###### Debian / Ubuntu:
```bash
sudo apt install libwebkit2gtk-4.1-dev
```
###### Fedora
```bash
sudo dnf install gtk3-devel webkit2gtk4.1-devel
```
###### Nix & NixOS
```nix
# shell.nix
let
# Unstable Channel | Rolling Release
pkgs = import (fetchTarball("channel:nixpkgs-unstable")) { };
packages = with pkgs; [
pkg-config
webkitgtk_4_1
];
in
pkgs.mkShell {
buildInputs = packages;
}
```
```sh
nix-shell shell.nix
```
###### GUIX
```scheme
;; manifest.scm
(specifications->manifest
'("pkg-config" ; Helper tool used when compiling
"webkitgtk" ; Web content engine fot GTK+
))
```
```bash
guix shell -m manifest.scm
```
#### macOS
WebKit is native on macOS so everything should be fine.
If you are cross-compiling for macOS using [osxcross](https://github.com/tpoechtrager/osxcross) and encounter a runtime panic like `Class with name WKWebViewConfiguration could not be found` it's possible that `WebKit.framework` has not been linked correctly, to fix this set the `RUSTFLAGS` environment variable:
```bash
RUSTFLAGS="-l framework=WebKit" cargo build --target=x86_64-apple-darwin --release
```
#### Windows
WebView2 provided by Microsoft Edge Chromium is used. So wry supports Windows 7, 8, 10 and 11.
#### Android
In order for `wry` to be able to create webviews on Android, there are a few requirements that your application needs to uphold:
1. You need to set a few environment variables that will be used to generate the necessary kotlin
files that you need to include in your Android application for wry to function properly.
- `WRY_ANDROID_PACKAGE`: which is the reversed domain name of your android project and the app name in snake_case, for example, `com.wry.example.wry_app`
- `WRY_ANDROID_LIBRARY`: for example, if your cargo project has a lib name `wry_app`, it will generate `libwry_app.so` so you set this env var to `wry_app`
- `WRY_ANDROID_KOTLIN_FILES_OUT_DIR`: for example, `path/to/app/src/main/kotlin/com/wry/example`
2. Your main Android Activity needs to inherit `AppCompatActivity`, preferably it should use the generated `WryActivity` or inherit it.
3. Your Rust app needs to call `wry::android_setup` function to setup the necessary logic to be able to create webviews later on.
4. Your Rust app needs to call `wry::android_binding!` macro to setup the JNI functions that will be called by `WryActivity` and various other places.
It is recommended to use the [`tao`](https://docs.rs/tao/latest/tao/) crate as it provides maximum compatibility with `wry`.
```rust
#[cfg(target_os = "android")]
{
tao::android_binding!(
com_example,
wry_app,
WryActivity,
wry::android_setup, // pass the wry::android_setup function to tao which will be invoked when the event loop is created
_start_app
);
wry::android_binding!(com_example, ttt);
}
```
If this feels overwhelming, you can just use the preconfigured template from [`cargo-mobile2`](https://github.com/tauri-apps/cargo-mobile2).
For more information, check out [MOBILE.md](https://github.com/tauri-apps/wry/blob/dev/MOBILE.md).
### Feature flags
Wry uses a set of feature flags to toggle several advanced features.
- `os-webview` (default): Enables the default WebView framework on the platform. This must be enabled
for the crate to work. This feature was added in preparation of other ports like cef and servo.
- `protocol` (default): Enables [`WebViewBuilder::with_custom_protocol`] to define custom URL scheme for handling tasks like
loading assets.
- `drag-drop` (default): Enables [`WebViewBuilder::with_drag_drop_handler`] to control the behavior when there are files
interacting with the window.
- `devtools`: Enables devtools on release builds. Devtools are always enabled in debug builds.
On **macOS**, enabling devtools, requires calling private APIs so you should not enable this flag in release
build if your app needs to publish to App Store.
- `transparent`: Transparent background on **macOS** requires calling private functions.
Avoid this in release build if your app needs to publish to App Store.
- `fullscreen`: Fullscreen video and other media on **macOS** requires calling private functions.
Avoid this in release build if your app needs to publish to App Store.
- `linux-body`: Enables body support of custom protocol request on Linux. Requires
WebKit2GTK v2.40 or above.
- `tracing`: enables [`tracing`] for `evaluate_script`, `ipc_handler`, and `custom_protocols`.
### Partners
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://crabnebula.dev" target="_blank">
<img src=".github/sponsors/crabnebula.svg" alt="CrabNebula" width="283">
</a>
</td>
</tr>
</tbody>
</table>
For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri).
### License
Apache-2.0/MIT
[`tao`]: https://docs.rs/tao
[`winit`]: https://docs.rs/winit
[`tracing`]: https://docs.rs/tracing

118
vendor/wry/build.rs vendored Normal file
View File

@ -0,0 +1,118 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os == "macos" || target_os == "ios" {
println!("cargo:rustc-link-lib=framework=WebKit");
}
if target_os == "android" {
use std::{fs, path::PathBuf};
fn env_var(var: &str) -> String {
std::env::var(var).unwrap_or_else(|_| {
panic!("`{var}` is not set, which is needed to generate the kotlin files for android.")
})
}
println!("cargo:rerun-if-env-changed=WRY_ANDROID_PACKAGE");
println!("cargo:rerun-if-env-changed=WRY_ANDROID_LIBRARY");
println!("cargo:rerun-if-env-changed=WRY_ANDROID_KOTLIN_FILES_OUT_DIR");
if let Ok(kotlin_out_dir) = std::env::var("WRY_ANDROID_KOTLIN_FILES_OUT_DIR") {
let package = env_var("WRY_ANDROID_PACKAGE");
let library = env_var("WRY_ANDROID_LIBRARY");
let kotlin_out_dir = PathBuf::from(&kotlin_out_dir)
.canonicalize()
.unwrap_or_else(move |_| {
panic!("Failed to canonicalize `WRY_ANDROID_KOTLIN_FILES_OUT_DIR` path {kotlin_out_dir}")
});
let kotlin_files_path =
PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("src/android/kotlin");
println!("cargo:rerun-if-changed={}", kotlin_files_path.display());
let kotlin_files = fs::read_dir(kotlin_files_path).expect("failed to read kotlin directory");
for file in kotlin_files {
let file = file.unwrap();
let class_extension_env = format!(
"WRY_{}_CLASS_EXTENSION",
file
.path()
.file_stem()
.unwrap()
.to_string_lossy()
.to_uppercase()
);
let class_init_env = format!(
"WRY_{}_CLASS_INIT",
file
.path()
.file_stem()
.unwrap()
.to_string_lossy()
.to_uppercase()
);
println!("cargo:rerun-if-env-changed={class_extension_env}");
println!("cargo:rerun-if-env-changed={class_init_env}");
let content = fs::read_to_string(file.path())
.expect("failed to read kotlin file as string")
.replace("{{package}}", &package)
.replace("{{package-unescaped}}", &package.replace('`', ""))
.replace("{{library}}", &library)
.replace(
"{{class-extension}}",
&std::env::var(&class_extension_env).unwrap_or_default(),
)
.replace(
"{{class-init}}",
&std::env::var(&class_init_env).unwrap_or_default(),
);
let auto_generated_comment = match file
.path()
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
{
"pro" => "# THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!!\n\n",
"kt" => "/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */\n\n",
_ => "String::new()",
};
let mut out = String::from(auto_generated_comment);
out.push_str(&content);
let out_path = kotlin_out_dir.join(file.file_name());
// Overwrite only if changed to not trigger rebuilds
if fs::read_to_string(&out_path).map_or(true, |o| o != out) {
fs::write(&out_path, out).expect("Failed to write kotlin file");
}
println!("cargo:rerun-if-changed={}", out_path.display());
}
}
}
let target = std::env::var("TARGET").unwrap_or_default();
let android = target.contains("android");
let linux = !android
&& (target.contains("linux")
|| target.contains("freebsd")
|| target.contains("dragonfly")
|| target.contains("netbsd")
|| target.contains("openbsd"));
alias("linux", linux);
alias("gtk", cfg!(feature = "os-webview") && linux);
}
fn alias(alias: &str, condition: bool) {
if condition {
println!("cargo:rustc-cfg={alias}");
}
}

483
vendor/wry/src/android/binding.rs vendored Normal file
View File

@ -0,0 +1,483 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use http::{
header::{HeaderName, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE},
Request,
};
use jni::errors::Result as JniResult;
pub use jni::{
self,
objects::{GlobalRef, JClass, JMap, JObject, JString},
sys::{jboolean, jint, jobject, jstring},
JNIEnv,
};
pub use ndk;
use ndk::looper::{FdEvent, ThreadLooper};
use std::os::fd::{AsFd, AsRawFd};
use super::{
main_pipe::{MainPipe, MAIN_PIPE},
ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER,
URL_LOADING_OVERRIDE, WITH_ASSET_LOADER,
};
use crate::PageLoadEvent;
#[macro_export]
macro_rules! android_binding {
($domain:ident, $package:ident) => {
::wry::android_binding!($domain, $package, ::wry)
};
// use imported `android_setup` just to force the import path to use `wry::{}`
// as the macro breaks without braces
($domain:ident, $package:ident, $wry:path) => {{
use $wry::{android_setup as _, prelude::*};
android_fn!($domain, $package, Rust, wryCreate, []);
android_fn!(
$domain,
$package,
Rust,
onWebviewDestroy,
[JObject, JString]
);
android_fn!(
$domain,
$package,
Rust,
handleRequest,
[JString, JObject, jboolean],
jobject
);
android_fn!(
$domain,
$package,
Rust,
withAssetLoader,
[JString],
jboolean
);
android_fn!(
$domain,
$package,
Rust,
assetLoaderDomain,
[JString],
jstring
);
android_fn!(
$domain,
$package,
Rust,
shouldOverride,
[JString, JString],
jboolean
);
android_fn!($domain, $package, Rust, onEval, [JString, jint, JString]);
android_fn!($domain, $package, Rust, onPageLoading, [JString, JString]);
android_fn!($domain, $package, Rust, onPageLoaded, [JString, JString]);
android_fn!($domain, $package, Rust, ipc, [JString, JString, JString]);
android_fn!(
$domain,
$package,
Rust,
handleReceivedTitle,
[JString, JString],
);
}};
}
fn handle_request(
env: &mut JNIEnv,
webview_id: JString,
request: JObject,
is_document_start_script_enabled: jboolean,
) -> JniResult<jobject> {
let webview_id = env.get_string(&webview_id)?;
let webview_id = webview_id.to_str().ok().unwrap_or_default();
if let Some(handler) = REQUEST_HANDLER.lock().unwrap().get(webview_id) {
#[cfg(feature = "tracing")]
let span =
tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty).entered();
let mut request_builder = Request::builder();
let uri = env
.call_method(&request, "getUrl", "()Landroid/net/Uri;", &[])?
.l()?;
let url: JString = env
.call_method(&uri, "toString", "()Ljava/lang/String;", &[])?
.l()?
.into();
let url = env.get_string(&url)?.to_string_lossy().to_string();
#[cfg(feature = "tracing")]
span.record("uri", &url);
request_builder = request_builder.uri(&url);
let method = env
.call_method(&request, "getMethod", "()Ljava/lang/String;", &[])?
.l()
.map(JString::from)?;
request_builder = request_builder.method(
env
.get_string(&method)?
.to_string_lossy()
.to_string()
.as_str(),
);
let request_headers = env
.call_method(request, "getRequestHeaders", "()Ljava/util/Map;", &[])?
.l()?;
let request_headers = JMap::from_env(env, &request_headers)?;
let mut iter = request_headers.iter(env)?;
while let Some((header, value)) = iter.next(env)? {
let header = JString::from(header);
let value = JString::from(value);
let header = env.get_string(&header)?;
let value = env.get_string(&value)?;
if let (Ok(header), Ok(value)) = (
HeaderName::from_bytes(header.to_bytes()),
HeaderValue::from_bytes(value.to_bytes()),
) {
request_builder = request_builder.header(header, value);
}
}
let final_request = match request_builder.body(Vec::new()) {
Ok(req) => req,
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to build response: {_e}");
return Ok(*JObject::null());
}
};
let response = {
#[cfg(feature = "tracing")]
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
(handler.handler)(
webview_id,
final_request,
is_document_start_script_enabled != 0,
)
};
if let Some(response) = response {
let status = response.status();
let status_code = status.as_u16() as i32;
let status_err = if status_code < 100 {
Some("Status code can't be less than 100")
} else if status_code > 599 {
Some("statusCode can't be greater than 599.")
} else if status_code > 299 && status_code < 400 {
Some("statusCode can't be in the [300, 399] range.")
} else {
None
};
if let Some(_err) = status_err {
#[cfg(feature = "tracing")]
tracing::warn!("{_err}");
return Ok(*JObject::null());
}
let reason_phrase = status.canonical_reason().unwrap_or("OK");
let (mime_type, encoding) = if let Some(content_type) = response.headers().get(CONTENT_TYPE) {
let content_type = content_type.to_str().unwrap().trim();
let mut s = content_type.split(';');
let mime_type = s.next().unwrap().trim();
let mut encoding = None;
for token in s {
let token = token.trim();
if token.starts_with("charset=") {
encoding.replace(token.split('=').nth(1).unwrap());
break;
}
}
(
env.new_string(mime_type)?,
if let Some(encoding) = encoding {
env.new_string(encoding)?
} else {
JString::default()
},
)
} else {
(JString::default(), JString::default())
};
let headers = response.headers();
let obj = env.new_object("java/util/HashMap", "()V", &[])?;
let response_headers = {
let headers_map = JMap::from_env(env, &obj)?;
for (name, value) in headers.iter() {
// WebResourceResponse will automatically generate Content-Type and
// Content-Length headers so we should skip them to avoid duplication.
if name == CONTENT_TYPE || name == CONTENT_LENGTH {
continue;
}
let key = env.new_string(name)?;
let value = env.new_string(value.to_str().unwrap_or_default())?;
headers_map.put(env, &key, &value)?;
}
headers_map
};
let bytes = response.body();
let byte_array_input_stream = env.find_class("java/io/ByteArrayInputStream")?;
let byte_array = env.byte_array_from_slice(bytes)?;
let stream = env.new_object(byte_array_input_stream, "([B)V", &[(&byte_array).into()])?;
let reason_phrase = env.new_string(reason_phrase)?;
let web_resource_response_class = env.find_class("android/webkit/WebResourceResponse")?;
let web_resource_response = env.new_object(
web_resource_response_class,
"(Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/util/Map;Ljava/io/InputStream;)V",
&[(&mime_type).into(), (&encoding).into(), status_code.into(), (&reason_phrase).into(), (&response_headers).into(), (&stream).into()],
)?;
return Ok(*web_resource_response);
}
}
Ok(*JObject::null())
}
#[allow(non_snake_case)]
pub unsafe fn wryCreate(env: JNIEnv, _: JClass) {
let mut main_pipe = MainPipe { env };
let looper = ThreadLooper::for_thread().unwrap();
looper
.add_fd_with_callback(MAIN_PIPE[0].as_fd(), FdEvent::INPUT, move |fd, _event| {
let size = std::mem::size_of::<bool>();
let mut wake = false;
if libc::read(fd.as_raw_fd(), &mut wake as *mut _ as *mut _, size) == size as libc::ssize_t {
// unregister itself on errors
main_pipe.recv().is_ok()
} else {
// unregister itself
false
}
})
.unwrap();
}
#[allow(non_snake_case)]
pub unsafe fn onWebviewDestroy(mut env: JNIEnv, _: JClass, activity: JObject, webview_id: JString) {
let activity_id = env
.call_method(&activity, "getId", "()I", &[])
.unwrap()
.i()
.unwrap();
let webview_id = env
.get_string(&webview_id)
.unwrap()
.to_string_lossy()
.to_string();
let is_changing_configurations = env
.call_method(&activity, "isChangingConfigurations", "()Z", &[])
.unwrap()
.z()
.unwrap();
super::MainPipe::send(
activity_id,
super::WebViewMessage::OnDestroy {
activity_id,
webview_id,
is_changing_configurations,
},
);
}
#[allow(non_snake_case)]
pub unsafe fn handleRequest(
mut env: JNIEnv,
_: JClass,
webview_id: JString,
request: JObject,
is_document_start_script_enabled: jboolean,
) -> jobject {
match handle_request(
&mut env,
webview_id,
request,
is_document_start_script_enabled,
) {
Ok(response) => response,
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to handle request: {_e}");
JObject::null().as_raw()
}
}
}
#[allow(non_snake_case)]
pub unsafe fn shouldOverride(
mut env: JNIEnv,
_: JClass,
webview_id: JString,
url: JString,
) -> jboolean {
match env.get_string(&url) {
Ok(url) => {
let url = url.to_string_lossy().to_string();
let Ok(webview_id) = env.get_string(&webview_id) else {
return false.into();
};
let webview_id = webview_id.to_str().ok().unwrap_or_default();
URL_LOADING_OVERRIDE
.lock()
.unwrap()
.get(webview_id)
// We negate the result of the function because the logic for the android
// client is different from how the navigation_handler is defined.
//
// https://developer.android.com/reference/android/webkit/WebViewClient#shouldOverrideUrlLoading(android.webkit.WebView,%20android.webkit.WebResourceRequest)
.map(|f| !(f.handler)(url))
.unwrap_or_default()
}
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}");
false
}
}
.into()
}
#[allow(non_snake_case)]
pub unsafe fn onEval(mut env: JNIEnv, _: JClass, _webview_id: JString, id: jint, result: JString) {
match env.get_string(&result) {
Ok(result) => {
if let Some(cb) = EVAL_CALLBACKS
.get_or_init(Default::default)
.lock()
.unwrap()
.get(&id)
{
cb(result.into());
}
}
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}");
}
}
}
pub unsafe fn ipc(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString, body: JString) {
match (
env.get_string(&url),
env.get_string(&body),
env.get_string(&webview_id),
) {
(Ok(url), Ok(body), Ok(webview_id)) => {
#[cfg(feature = "tracing")]
let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered();
let url = url.to_string_lossy().to_string();
let body = body.to_string_lossy().to_string();
let webview_id = webview_id.to_string_lossy().to_string();
if let Some(ipc) = IPC.lock().unwrap().get(&webview_id) {
(ipc.handler)(Request::builder().uri(url).body(body).unwrap())
}
}
(Err(_e), _, _) | (_, Err(_e), _) | (_, _, Err(_e)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
#[allow(non_snake_case)]
pub unsafe fn handleReceivedTitle(mut env: JNIEnv, _: JClass, webview_id: JString, title: JString) {
match (env.get_string(&title), env.get_string(&webview_id)) {
(Ok(title), Ok(webview_id)) => {
let title = title.to_string_lossy().to_string();
let webview_id = webview_id.to_string_lossy().to_string();
if let Some(title_handler) = TITLE_CHANGE_HANDLER.lock().unwrap().get(&webview_id) {
(title_handler.handler)(title)
}
}
(Err(_e), _) | (_, Err(_e)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
#[allow(non_snake_case)]
pub unsafe fn withAssetLoader(mut env: JNIEnv, _: JClass, webview_id: JString) -> jboolean {
let Ok(webview_id) = env.get_string(&webview_id) else {
return false.into();
};
let webview_id = webview_id.to_str().ok().unwrap_or_default();
(*WITH_ASSET_LOADER
.lock()
.unwrap()
.get(webview_id)
.unwrap_or(&false))
.into()
}
#[allow(non_snake_case)]
pub unsafe fn assetLoaderDomain(mut env: JNIEnv, _: JClass, webview_id: JString) -> jstring {
let Ok(webview_id) = env.get_string(&webview_id) else {
return env.new_string("wry.assets").unwrap().as_raw();
};
let webview_id = webview_id.to_str().ok().unwrap_or_default();
if let Some(domain) = ASSET_LOADER_DOMAIN.lock().unwrap().get(webview_id) {
env.new_string(domain).unwrap().as_raw()
} else {
env.new_string("wry.assets").unwrap().as_raw()
}
}
#[allow(non_snake_case)]
pub unsafe fn onPageLoading(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString) {
match (env.get_string(&url), env.get_string(&webview_id)) {
(Ok(url), Ok(webview_id)) => {
let url = url.to_string_lossy().to_string();
let webview_id = webview_id.to_string_lossy().to_string();
if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().get(&webview_id) {
(on_load.handler)(PageLoadEvent::Started, url)
}
}
(Err(_e), _) | (_, Err(_e)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}
#[allow(non_snake_case)]
pub unsafe fn onPageLoaded(mut env: JNIEnv, _: JClass, webview_id: JString, url: JString) {
match (env.get_string(&url), env.get_string(&webview_id)) {
(Ok(url), Ok(webview_id)) => {
let url = url.to_string_lossy().to_string();
let webview_id = webview_id.to_string_lossy().to_string();
if let Some(on_load) = ON_LOAD_HANDLER.lock().unwrap().get(&webview_id) {
(on_load.handler)(PageLoadEvent::Finished, url)
}
}
(Err(_e), _) | (_, Err(_e)) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to parse JString: {_e}")
}
}
}

23
vendor/wry/src/android/kotlin/Ipc.kt vendored Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused")
package {{package}}
import android.webkit.*
class Ipc(val webView: RustWebView, val webViewClient: RustWebViewClient) {
@JavascriptInterface
fun postMessage(message: String?) {
message?.let {m ->
// we're not using WebView::getUrl() here because it needs to be executed on the main thread
// and it would slow down the Ipc
// so instead we track the current URL on the webview client
Rust.ipc(webView.id, webViewClient.currentUrl, m)
}
}
{{class-extension}}
}

87
vendor/wry/src/android/kotlin/Logger.kt vendored Normal file
View File

@ -0,0 +1,87 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package {{package}}
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java
import android.text.TextUtils
import android.util.Log
class Logger {
companion object {
private const val LOG_TAG_CORE = "Tauri"
fun tags(vararg subtags: String): String {
return if (subtags.isNotEmpty()) {
LOG_TAG_CORE + "/" + TextUtils.join("/", subtags)
} else LOG_TAG_CORE
}
fun verbose(message: String) {
verbose(LOG_TAG_CORE, message)
}
private fun verbose(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.v(tag, message)
}
fun debug(message: String) {
debug(LOG_TAG_CORE, message)
}
fun debug(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.d(tag, message)
}
fun info(message: String) {
info(LOG_TAG_CORE, message)
}
fun info(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.i(tag, message)
}
fun warn(message: String) {
warn(LOG_TAG_CORE, message)
}
fun warn(tag: String, message: String) {
if (!shouldLog()) {
return
}
Log.w(tag, message)
}
fun error(message: String) {
error(LOG_TAG_CORE, message, null)
}
fun error(message: String, e: Throwable?) {
error(LOG_TAG_CORE, message, e)
}
fun error(tag: String, message: String, e: Throwable?) {
if (!shouldLog()) {
return
}
Log.e(tag, message, e)
}
private fun shouldLog(): Boolean {
return BuildConfig.DEBUG
}
}
}

View File

@ -0,0 +1,115 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{package}}
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import java.util.ArrayList
object PermissionHelper {
/**
* Checks if a list of given permissions are all granted by the user
*
* @param permissions Permissions to check.
* @return True if all permissions are granted, false if at least one is not.
*/
fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
for (perm in permissions) {
if (ActivityCompat.checkSelfPermission(
context!!,
perm
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
/**
* Check whether the given permission has been defined in the AndroidManifest.xml
*
* @param permission A permission to check.
* @return True if the permission has been defined in the Manifest, false if not.
*/
fun hasDefinedPermission(context: Context, permission: String): Boolean {
var hasPermission = false
val requestedPermissions = getManifestPermissions(context)
if (!requestedPermissions.isNullOrEmpty()) {
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
if (requestedPermissionsArrayList.contains(permission)) {
hasPermission = true
}
}
return hasPermission
}
/**
* Check whether all of the given permissions have been defined in the AndroidManifest.xml
* @param context the app context
* @param permissions a list of permissions
* @return true only if all permissions are defined in the AndroidManifest.xml
*/
fun hasDefinedPermissions(context: Context, permissions: Array<String>): Boolean {
for (permission in permissions) {
if (!hasDefinedPermission(context, permission)) {
return false
}
}
return true
}
/**
* Get the permissions defined in AndroidManifest.xml
*
* @return The permissions defined in AndroidManifest.xml
*/
private fun getManifestPermissions(context: Context): Array<String>? {
var requestedPermissions: Array<String>? = null
try {
val pm = context.packageManager
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
}
if (packageInfo != null) {
requestedPermissions = packageInfo.requestedPermissions
}
} catch (_: Exception) {
}
return requestedPermissions
}
/**
* Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
*
* @param neededPermissions The permissions needed.
* @return The permissions not present in AndroidManifest.xml
*/
fun getUndefinedPermissions(context: Context, neededPermissions: Array<String?>): Array<String?> {
val undefinedPermissions = ArrayList<String?>()
val requestedPermissions = getManifestPermissions(context)
if (!requestedPermissions.isNullOrEmpty()) {
val requestedPermissionsList = listOf(*requestedPermissions)
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
for (permission in neededPermissions) {
if (!requestedPermissionsArrayList.contains(permission)) {
undefinedPermissions.add(permission)
}
}
var undefinedPermissionArray = arrayOfNulls<String>(undefinedPermissions.size)
undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray)
return undefinedPermissionArray
}
return neededPermissions
}
}

46
vendor/wry/src/android/kotlin/Rust.kt vendored Normal file
View File

@ -0,0 +1,46 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused")
package {{package}}
import android.content.Intent
import android.webkit.WebView
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
object Rust {
init {
System.loadLibrary("{{library}}")
}
@JvmStatic external fun onActivityCreate(activity: WryActivity)
@JvmStatic external fun onActivityDestroy(activity: WryActivity)
@JvmStatic external fun onActivitySaveInstanceState()
@JvmStatic external fun onActivityLowMemory()
@JvmStatic external fun onWindowFocusChanged(activity: WryActivity, focus: Boolean)
@JvmStatic external fun onNewIntent(intent: Intent)
@JvmStatic external fun create()
@JvmStatic external fun start()
@JvmStatic external fun resume()
@JvmStatic external fun pause()
@JvmStatic external fun stop()
@JvmStatic external fun wryCreate()
@JvmStatic external fun onWebviewDestroy(activity: WryActivity, webviewId: String)
@JvmStatic external fun ipc(webviewId: String, url: String, message: String)
@JvmStatic external fun assetLoaderDomain(webviewId: String): String
@JvmStatic external fun withAssetLoader(webviewId: String): Boolean
@JvmStatic external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse?
@JvmStatic external fun shouldOverride(webviewId: String, url: String): Boolean
@JvmStatic external fun onPageLoading(webviewId: String, url: String)
@JvmStatic external fun onPageLoaded(webviewId: String, url: String)
@JvmStatic external fun onEval(webviewId: String, id: Int, result: String)
@JvmStatic external fun handleReceivedTitle(webviewId: String, title: String)
}

View File

@ -0,0 +1,491 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("ObsoleteSdkInt", "RedundantOverride", "QueryPermissionsNeeded", "SimpleDateFormat")
package {{package}}
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.view.View
import android.webkit.*
import android.widget.EditText
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
private interface PermissionListener {
fun onPermissionSelect(isGranted: Boolean?)
}
private interface ActivityResultListener {
fun onActivityResult(result: ActivityResult?)
}
private val activity: WryActivity
private var permissionLauncher: ActivityResultLauncher<Array<String>>
private var activityLauncher: ActivityResultLauncher<Intent>
private var permissionListener: PermissionListener? = null
private var activityListener: ActivityResultListener? = null
init {
activity = appActivity
val permissionCallback =
ActivityResultCallback { isGranted: Map<String, Boolean> ->
if (permissionListener != null) {
var granted = true
for ((_, value) in isGranted) {
if (!value) granted = false
}
permissionListener!!.onPermissionSelect(granted)
}
}
permissionLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback)
activityLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (activityListener != null) {
activityListener!!.onActivityResult(result)
}
}
}
/**
* Render web content in `view`.
*
* Both this method and [.onHideCustomView] are required for
* rendering web content in full screen.
*
* @see [](https://developer.android.com/reference/android/webkit/WebChromeClient.onShowCustomView
) */
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
callback.onCustomViewHidden()
super.onShowCustomView(view, callback)
}
/**
* Render web content in the original Web View again.
*
* Do not remove this method--@see #onShowCustomView(View, CustomViewCallback).
*/
override fun onHideCustomView() {
super.onHideCustomView()
}
override fun onPermissionRequest(request: PermissionRequest) {
val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
val permissionList: MutableList<String> = ArrayList()
if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) {
permissionList.add(Manifest.permission.CAMERA)
}
if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) {
permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS)
permissionList.add(Manifest.permission.RECORD_AUDIO)
}
if (permissionList.isNotEmpty() && isRequestPermissionRequired) {
val permissions = permissionList.toTypedArray()
permissionListener = object : PermissionListener {
override fun onPermissionSelect(isGranted: Boolean?) {
if (isGranted == true) {
request.grant(request.resources)
} else {
request.deny()
}
}
}
permissionLauncher.launch(permissions)
} else {
request.grant(request.resources)
}
}
/**
* Show the browser alert modal
* @param view
* @param url
* @param message
* @param result
* @return
*/
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
if (activity.isFinishing) {
return true
}
val builder = AlertDialog.Builder(view.context)
builder
.setMessage(message)
.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.confirm()
}
.setOnCancelListener { dialog: DialogInterface ->
dialog.dismiss()
result.cancel()
}
val dialog = builder.create()
dialog.show()
return true
}
/**
* Show the browser confirm modal
* @param view
* @param url
* @param message
* @param result
* @return
*/
override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult): Boolean {
if (activity.isFinishing) {
return true
}
val builder = AlertDialog.Builder(view.context)
builder
.setMessage(message)
.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.confirm()
}
.setNegativeButton(
"Cancel"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.cancel()
}
.setOnCancelListener { dialog: DialogInterface ->
dialog.dismiss()
result.cancel()
}
val dialog = builder.create()
dialog.show()
return true
}
/**
* Show the browser prompt modal
* @param view
* @param url
* @param message
* @param defaultValue
* @param result
* @return
*/
override fun onJsPrompt(
view: WebView,
url: String,
message: String,
defaultValue: String,
result: JsPromptResult
): Boolean {
if (activity.isFinishing) {
return true
}
val builder = AlertDialog.Builder(view.context)
val input = EditText(view.context)
builder
.setMessage(message)
.setView(input)
.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
val inputText1 = input.text.toString().trim { it <= ' ' }
result.confirm(inputText1)
}
.setNegativeButton(
"Cancel"
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
result.cancel()
}
.setOnCancelListener { dialog: DialogInterface ->
dialog.dismiss()
result.cancel()
}
val dialog = builder.create()
dialog.show()
return true
}
/**
* Handle the browser geolocation permission prompt
* @param origin
* @param callback
*/
override fun onGeolocationPermissionsShowPrompt(
origin: String,
callback: GeolocationPermissions.Callback
) {
super.onGeolocationPermissionsShowPrompt(origin, callback)
Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin")
val geoPermissions =
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
if (!PermissionHelper.hasPermissions(activity, geoPermissions)) {
permissionListener = object : PermissionListener {
override fun onPermissionSelect(isGranted: Boolean?) {
if (isGranted == true) {
callback.invoke(origin, true, false)
} else {
val coarsePermission =
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
PermissionHelper.hasPermissions(activity, coarsePermission)
) {
callback.invoke(origin, true, false)
} else {
callback.invoke(origin, false, false)
}
}
}
}
permissionLauncher.launch(geoPermissions)
} else {
// permission is already granted
callback.invoke(origin, true, false)
Logger.debug("onGeolocationPermissionsShowPrompt: has required permission")
}
}
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams
): Boolean {
val acceptTypes = listOf(*fileChooserParams.acceptTypes)
val captureEnabled = fileChooserParams.isCaptureEnabled
val capturePhoto = captureEnabled && acceptTypes.contains("image/*")
val captureVideo = captureEnabled && acceptTypes.contains("video/*")
if (capturePhoto || captureVideo) {
if (isMediaCaptureSupported) {
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
} else {
permissionListener = object : PermissionListener {
override fun onPermissionSelect(isGranted: Boolean?) {
if (isGranted == true) {
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
} else {
Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted")
filePathCallback.onReceiveValue(null)
}
}
}
val camPermission = arrayOf(Manifest.permission.CAMERA)
permissionLauncher.launch(camPermission)
}
} else {
showFilePicker(filePathCallback, fileChooserParams)
}
return true
}
private val isMediaCaptureSupported: Boolean
get() {
val permissions = arrayOf(Manifest.permission.CAMERA)
return PermissionHelper.hasPermissions(activity, permissions) ||
!PermissionHelper.hasDefinedPermission(activity, Manifest.permission.CAMERA)
}
private fun showMediaCaptureOrFilePicker(
filePathCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams,
isVideo: Boolean
) {
val isVideoCaptureSupported = true
val shown = if (isVideo && isVideoCaptureSupported) {
showVideoCapturePicker(filePathCallback)
} else {
showImageCapturePicker(filePathCallback)
}
if (!shown) {
Logger.warn(
Logger.tags("FileChooser"),
"Media capture intent could not be launched. Falling back to default file picker."
)
showFilePicker(filePathCallback, fileChooserParams)
}
}
private fun showImageCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (takePictureIntent.resolveActivity(activity.packageManager) == null) {
return false
}
val imageFileUri: Uri = try {
createImageFileUri()
} catch (ex: Exception) {
Logger.error("Unable to create temporary media capture file: " + ex.message)
return false
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri)
activityListener = object : ActivityResultListener {
override fun onActivityResult(result: ActivityResult?) {
var res: Array<Uri?>? = null
if (result?.resultCode == Activity.RESULT_OK) {
res = arrayOf(imageFileUri)
}
filePathCallback.onReceiveValue(res)
}
}
activityLauncher.launch(takePictureIntent)
return true
}
private fun showVideoCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
if (takeVideoIntent.resolveActivity(activity.packageManager) == null) {
return false
}
activityListener = object : ActivityResultListener {
override fun onActivityResult(result: ActivityResult?) {
var res: Array<Uri?>? = null
if (result?.resultCode == Activity.RESULT_OK) {
res = arrayOf(result.data!!.data)
}
filePathCallback.onReceiveValue(res)
}
}
activityLauncher.launch(takeVideoIntent)
return true
}
private fun showFilePicker(
filePathCallback: ValueCallback<Array<Uri?>?>,
fileChooserParams: FileChooserParams
) {
val intent = fileChooserParams.createIntent()
if (fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
if (fileChooserParams.acceptTypes.size > 1 || intent.type!!.startsWith(".")) {
val validTypes = getValidTypes(fileChooserParams.acceptTypes)
intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes)
if (intent.type!!.startsWith(".")) {
intent.type = validTypes[0]
}
}
try {
activityListener = object : ActivityResultListener {
override fun onActivityResult(result: ActivityResult?) {
val res: Array<Uri?>?
val resultIntent = result?.data
if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) {
val numFiles = resultIntent.clipData!!.itemCount
res = arrayOfNulls(numFiles)
for (i in 0 until numFiles) {
res[i] = resultIntent.clipData!!.getItemAt(i).uri
}
} else {
res = FileChooserParams.parseResult(
result?.resultCode ?: 0,
resultIntent
)
}
filePathCallback.onReceiveValue(res)
}
}
activityLauncher.launch(intent)
} catch (e: ActivityNotFoundException) {
filePathCallback.onReceiveValue(null)
}
}
private fun getValidTypes(currentTypes: Array<String>): Array<String> {
val validTypes: MutableList<String> = ArrayList()
val mtm = MimeTypeMap.getSingleton()
for (mime in currentTypes) {
if (mime.startsWith(".")) {
val extension = mime.substring(1)
val extensionMime = mtm.getMimeTypeFromExtension(extension)
if (extensionMime != null && !validTypes.contains(extensionMime)) {
validTypes.add(extensionMime)
}
} else if (!validTypes.contains(mime)) {
validTypes.add(mime)
}
}
val validObj: Array<Any> = validTypes.toTypedArray()
return Arrays.copyOf(
validObj, validObj.size,
Array<String>::class.java
)
}
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
val tag: String = Logger.tags("Console")
if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
val msg = String.format(
"File: %s - Line %d - Msg: %s",
consoleMessage.sourceId(),
consoleMessage.lineNumber(),
consoleMessage.message()
)
val level = consoleMessage.messageLevel().name
if ("ERROR".equals(level, ignoreCase = true)) {
Logger.error(tag, msg, null)
} else if ("WARNING".equals(level, ignoreCase = true)) {
Logger.warn(tag, msg)
} else if ("TIP".equals(level, ignoreCase = true)) {
Logger.debug(tag, msg)
} else {
Logger.info(tag, msg)
}
}
return true
}
private fun isValidMsg(msg: String): Boolean {
return !(msg.contains("%cresult %c") ||
msg.contains("%cnative %c") ||
msg.equals("[object Object]", ignoreCase = true) ||
msg.equals("console.groupEnd", ignoreCase = true))
}
@Throws(IOException::class)
private fun createImageFileUri(): Uri {
val photoFile = createImageFile(activity)
return FileProvider.getUriForFile(
activity,
activity.packageName.toString() + ".fileprovider",
photoFile
)
}
@Throws(IOException::class)
private fun createImageFile(activity: Activity): File {
// Create an image file name
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(imageFileName, ".jpg", storageDir)
}
override fun onReceivedTitle(
view: WebView,
title: String
) {
Rust.handleReceivedTitle((view as RustWebView).id, title)
}
}

View File

@ -0,0 +1,96 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
@file:Suppress("unused", "SetJavaScriptEnabled")
package {{package}}
import android.annotation.SuppressLint
import android.webkit.*
import android.content.Context
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import kotlin.collections.Map
@SuppressLint("RestrictedApi")
class RustWebView(context: Context, val initScripts: Array<String>, val id: String): WebView(context) {
val isDocumentStartScriptEnabled: Boolean
init {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.setGeolocationEnabled(true)
settings.databaseEnabled = true
settings.mediaPlaybackRequiresUserGesture = false
settings.javaScriptCanOpenWindowsAutomatically = true
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
isDocumentStartScriptEnabled = true
for (script in initScripts) {
WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*"));
}
} else {
isDocumentStartScriptEnabled = false
}
{{class-init}}
}
fun loadUrlMainThread(url: String) {
post {
loadUrl(url)
}
}
fun loadUrlMainThread(url: String, additionalHttpHeaders: Map<String, String>) {
post {
loadUrl(url, additionalHttpHeaders)
}
}
override fun loadUrl(url: String) {
if (!Rust.shouldOverride(id, url)) {
super.loadUrl(url);
}
}
override fun loadUrl(url: String, additionalHttpHeaders: Map<String, String>) {
if (!Rust.shouldOverride(id, url)) {
super.loadUrl(url, additionalHttpHeaders);
}
}
fun loadHTMLMainThread(html: String) {
post {
super.loadData(html, "text/html", null)
}
}
fun evalScript(id: Int, script: String) {
post {
super.evaluateJavascript(script) { result ->
Rust.onEval(this.id, id, result)
}
}
}
fun clearAllBrowsingData() {
try {
super.getContext().deleteDatabase("webviewCache.db")
super.getContext().deleteDatabase("webview.db")
super.clearCache(true)
super.clearHistory()
super.clearFormData()
} catch (ex: Exception) {
Logger.error("Unable to create temporary media capture file: " + ex.message)
}
}
fun getCookies(url: String): String {
val cookieManager = CookieManager.getInstance()
return cookieManager.getCookie(url)
}
{{class-extension}}
}

View File

@ -0,0 +1,98 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{package}}
import android.net.Uri
import android.webkit.*
import android.content.Context
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import androidx.webkit.WebViewAssetLoader
class RustWebViewClient(webView: RustWebView, context: Context): WebViewClient() {
private val interceptedState = mutableMapOf<String, Boolean>()
var currentUrl: String = "about:blank"
private var lastInterceptedUrl: Uri? = null
private var pendingUrlRedirect: String? = null
private val assetLoader = WebViewAssetLoader.Builder()
.setDomain(Rust.assetLoaderDomain(webView.id))
.addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context))
.build()
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
pendingUrlRedirect?.let {
Handler(Looper.getMainLooper()).post {
view.loadUrl(it)
}
pendingUrlRedirect = null
return null
}
lastInterceptedUrl = request.url
return if (Rust.withAssetLoader((view as RustWebView).id)) {
assetLoader.shouldInterceptRequest(request.url)
} else {
val response = Rust.handleRequest(view.id, request, view.isDocumentStartScriptEnabled)
if (response != null) {
if (response.responseHeaders != null) {
response.responseHeaders["Cache-Control"] = "no-store"
} else {
response.responseHeaders = mapOf("Cache-Control" to "no-store")
}
}
interceptedState[request.url.toString()] = response != null
return response
}
}
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return Rust.shouldOverride((view as RustWebView).id, request.url.toString())
}
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
currentUrl = url
if (interceptedState[url] == false) {
val webView = view as RustWebView
for (script in webView.initScripts) {
view.evaluateJavascript(script, null)
}
}
return Rust.onPageLoading((view as RustWebView).id, url)
}
override fun onPageFinished(view: WebView, url: String) {
Rust.onPageLoaded((view as RustWebView).id, url)
}
override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError
) {
// we get a net::ERR_CONNECTION_REFUSED when an external URL redirects to a custom protocol
// e.g. oauth flow, because shouldInterceptRequest is not called on redirects
// so we must force retry here with loadUrl() to get a chance of the custom protocol to kick in
if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) {
// prevent the default error page from showing
view.stopLoading()
// without this initial loadUrl the app is stuck
view.loadUrl(request.url.toString())
// ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later
pendingUrlRedirect = request.url.toString()
} else {
super.onReceivedError(view, request, error)
}
}
{{class-extension}}
}

View File

@ -0,0 +1,173 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package {{package}}
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.webkit.WebView
import android.view.KeyEvent
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
private val ACTIVITY_ID_KEY = "__wryActivityId"
object WryLifecycleObserver : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
Rust.create()
Rust.wryCreate()
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
Rust.start()
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
Rust.resume()
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
Rust.pause()
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
Rust.stop()
}
}
abstract class WryActivity : AppCompatActivity() {
private lateinit var mWebView: RustWebView
var id: Int = 0
open val handleBackNavigation: Boolean = true
open fun onWebViewCreate(webView: WebView) { }
fun setWebView(webView: RustWebView) {
mWebView = webView
if (handleBackNavigation) {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (this@WryActivity::mWebView.isInitialized) {
if (this@WryActivity.mWebView.canGoBack()) {
this@WryActivity.mWebView.goBack()
} else {
this.isEnabled = false
this@WryActivity.onBackPressed()
this.isEnabled = true
}
}
}
}
onBackPressedDispatcher.addCallback(this, callback)
}
onWebViewCreate(webView)
}
val version: String
@SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt")
get() {
// Check getCurrentWebViewPackage() directly if above Android 8
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return WebView.getCurrentWebViewPackage()?.versionName ?: ""
}
// Otherwise manually check WebView versions
var webViewPackage = "com.google.android.webview"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
webViewPackage = "com.android.chrome"
}
try {
@Suppress("DEPRECATION")
val info = packageManager.getPackageInfo(webViewPackage, 0)
return info.versionName.toString()
} catch (ex: Exception) {
Logger.warn("Unable to get package info for '$webViewPackage'$ex")
}
try {
@Suppress("DEPRECATION")
val info = packageManager.getPackageInfo("com.android.webview", 0)
return info.versionName.toString()
} catch (ex: Exception) {
Logger.warn("Unable to get package info for 'com.android.webview'$ex")
}
// Could not detect any webview, return empty string
return ""
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
id = savedInstanceState?.getInt(ACTIVITY_ID_KEY) ?: intent.extras?.getInt(ACTIVITY_ID_KEY) ?: hashCode()
ProcessLifecycleOwner.get().lifecycle.addObserver(WryLifecycleObserver)
Rust.onActivityCreate(this)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
Rust.onWindowFocusChanged(this, hasFocus)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(ACTIVITY_ID_KEY, id)
Rust.onActivitySaveInstanceState()
}
override fun onPause() {
super.onPause()
if (::mWebView.isInitialized) {
mWebView.onPause()
}
}
override fun onResume() {
super.onResume()
if (::mWebView.isInitialized) {
mWebView.onResume()
}
}
override fun onDestroy() {
super.onDestroy()
Rust.onActivityDestroy(this)
Rust.onWebviewDestroy(this, if (::mWebView.isInitialized) { mWebView.id } else { "" })
}
override fun onLowMemory() {
super.onLowMemory()
Rust.onActivityLowMemory()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Rust.onNewIntent(intent)
}
fun getAppClass(name: String): Class<*> {
return Class.forName(name)
}
fun startActivity(cls: Class<*>): Int {
val intent = Intent(this, cls)
val id = kotlin.random.Random.nextInt()
intent.putExtra(ACTIVITY_ID_KEY, id)
startActivity(intent)
return id
}
{{class-extension}}
}

View File

@ -0,0 +1,35 @@
# Copyright 2020-2023 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
-keep class {{package-unescaped}}.* {
native <methods>;
}
-keep class {{package-unescaped}}.WryActivity {
public <init>(...);
void setWebView({{package-unescaped}}.RustWebView);
java.lang.Class getAppClass(...);
int getId();
java.lang.String getVersion();
int startActivity(...);
}
-keep class {{package-unescaped}}.Ipc {
public <init>(...);
@android.webkit.JavascriptInterface public <methods>;
}
-keep class {{package-unescaped}}.RustWebView {
public <init>(...);
void loadUrlMainThread(...);
void loadHTMLMainThread(...);
void evalScript(...);
}
-keep class {{package-unescaped}}.RustWebChromeClient,{{package-unescaped}}.RustWebViewClient {
public <init>(...);
}

607
vendor/wry/src/android/main_pipe.rs vendored Normal file
View File

@ -0,0 +1,607 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{Error, InitializationScript, RGBA};
use crossbeam_channel::*;
use jni::{
errors::Result as JniResult,
objects::{GlobalRef, JMap, JObject, JString},
JNIEnv, JavaVM,
};
use once_cell::sync::Lazy;
use std::{
collections::BTreeMap,
ffi::c_void,
os::unix::prelude::*,
sync::{Arc, Mutex},
};
use super::{find_class, EvalCallback, WebviewId, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE};
pub type ActivityId = i32;
static CHANNEL: Lazy<(
Sender<(ActivityId, WebViewMessage)>,
Receiver<(ActivityId, WebViewMessage)>,
)> = Lazy::new(|| bounded(8));
pub static MAIN_PIPE: Lazy<[OwnedFd; 2]> = Lazy::new(|| {
let mut pipe: [RawFd; 2] = Default::default();
unsafe { libc::pipe(pipe.as_mut_ptr()) };
unsafe { pipe.map(|fd| OwnedFd::from_raw_fd(fd)) }
});
#[derive(Clone)]
pub struct ActivityProxy {
pub activity: GlobalRef,
pub window_manager: GlobalRef,
pub webview: Option<GlobalRef>,
pub webchrome_client: GlobalRef,
pub java_vm: *mut c_void,
}
unsafe impl Send for ActivityProxy {}
impl ActivityProxy {
pub fn new(
vm: JavaVM,
activity: GlobalRef,
window_manager: GlobalRef,
webchrome_client: GlobalRef,
) -> Self {
Self {
activity,
window_manager,
webview: None,
webchrome_client,
java_vm: vm.get_java_vm_pointer() as *mut _,
}
}
}
static ACTIVITY_PROXY: once_cell::sync::Lazy<Mutex<BTreeMap<ActivityId, ActivityProxy>>> =
Lazy::new(|| Mutex::new(BTreeMap::new()));
pub fn activity_proxy(id: ActivityId) -> Option<ActivityProxy> {
ACTIVITY_PROXY.lock().unwrap().get(&id).cloned()
}
fn remove_activity_proxy(id: ActivityId) {
ACTIVITY_PROXY.lock().unwrap().remove(&id);
}
pub fn register_activity_proxy(
vm: JavaVM,
id: ActivityId,
activity: GlobalRef,
window_manager: GlobalRef,
webchrome_client: GlobalRef,
) {
let mut activity_proxy = ACTIVITY_PROXY.lock().unwrap();
if let Some(proxy) = activity_proxy.get_mut(&id) {
proxy.activity = activity;
proxy.window_manager = window_manager;
proxy.webchrome_client = webchrome_client;
proxy.java_vm = vm.get_java_vm_pointer() as *mut _;
} else {
let proxy = ActivityProxy::new(vm, activity, window_manager, webchrome_client);
activity_proxy.insert(id, proxy.clone());
}
}
pub fn activity_id_for_window_manager(window_manager: JObject) -> Option<ActivityId> {
for (activity_id, proxy) in ACTIVITY_PROXY.lock().unwrap().iter() {
let vm = unsafe { JavaVM::from_raw(proxy.java_vm.cast()) }.unwrap();
let mut env = vm.attach_current_thread_as_daemon().unwrap();
let equals = env
.call_method(
proxy.window_manager.as_obj(),
"equals",
"(Ljava/lang/Object;)Z",
&[(&window_manager).into()],
)
.and_then(|v| v.z())
.unwrap_or_default();
if equals {
return Some(*activity_id);
}
}
None
}
pub fn first_activity_id() -> Option<ActivityId> {
ACTIVITY_PROXY.lock().unwrap().keys().next().cloned()
}
pub fn get_webview(activity_id: ActivityId) -> Option<GlobalRef> {
ACTIVITY_PROXY
.lock()
.unwrap()
.get(&activity_id)
.unwrap()
.webview
.as_ref()
.cloned()
}
pub struct MainPipe<'a> {
pub env: JNIEnv<'a>,
}
impl<'a> MainPipe<'a> {
pub(crate) fn send(activity_id: ActivityId, message: WebViewMessage) {
let size = std::mem::size_of::<bool>();
if CHANNEL.0.send((activity_id, message)).is_ok() {
unsafe {
libc::write(
MAIN_PIPE[1].as_raw_fd(),
&true as *const _ as *const _,
size,
)
};
}
}
pub fn recv(&mut self) -> JniResult<()> {
if let Ok((activity_id, message)) = CHANNEL.1.recv() {
match message {
WebViewMessage::CreateWebView(attrs) => {
let Some((activity, web_chrome_client)) =
activity_proxy(activity_id).map(|p| (p.activity.clone(), p.webchrome_client.clone()))
else {
#[cfg(debug_assertions)]
eprintln!("no activity found for activity id: {}", activity_id);
return Ok(());
};
let CreateWebViewAttributes {
url,
html,
#[cfg(any(debug_assertions, feature = "devtools"))]
devtools,
transparent,
background_color,
headers,
on_webview_created,
autoplay,
user_agent,
initialization_scripts,
id,
javascript_disabled,
..
} = attrs;
let string_class = self.env.find_class("java/lang/String")?;
let initialization_scripts_array = self.env.new_object_array(
initialization_scripts.len() as i32,
string_class,
self.env.new_string("")?,
)?;
for (i, init_script) in initialization_scripts.into_iter().enumerate() {
self.env.set_object_array_element(
&initialization_scripts_array,
i as i32,
self.env.new_string(init_script.script)?,
)?;
}
let id = self.env.new_string(id)?;
// Create webview
let rust_webview_class = find_class(
&mut self.env,
&activity,
format!("{}/RustWebView", PACKAGE.get().unwrap()),
)?;
let webview = self.env.new_object(
&rust_webview_class,
"(Landroid/content/Context;[Ljava/lang/String;Ljava/lang/String;)V",
&[
(&activity).into(),
(&initialization_scripts_array).into(),
(&id).into(),
],
)?;
// get settings
let web_settings = self
.env
.call_method(
&webview,
"getSettings",
"()Landroid/webkit/WebSettings;",
&[],
)?
.l()?;
// set media autoplay
self.env.call_method(
&web_settings,
"setMediaPlaybackRequiresUserGesture",
"(Z)V",
&[(!autoplay).into()],
)?;
// set user-agent
if let Some(user_agent) = user_agent {
let user_agent = self.env.new_string(user_agent)?;
self.env.call_method(
&web_settings,
"setUserAgentString",
"(Ljava/lang/String;)V",
&[(&user_agent).into()],
)?;
}
// disable javascript
if javascript_disabled {
self.env.call_method(
&web_settings,
"setJavaScriptEnabled",
"(Z)V",
&[false.into()],
)?;
}
let webview_class_name = format!("{}/RustWebView", PACKAGE.get().unwrap());
self.env.call_method(
&activity,
"setWebView",
format!("(L{webview_class_name};)V"),
&[(&webview).into()],
)?;
// Navigation
if let Some(u) = url {
if let Ok(url) = self.env.new_string(u) {
load_url(&mut self.env, &webview, &url, headers, true)?;
}
} else if let Some(h) = html {
if let Ok(html) = self.env.new_string(h) {
load_html(&mut self.env, &webview, &html)?;
}
}
// Enable devtools
#[cfg(any(debug_assertions, feature = "devtools"))]
self.env.call_static_method(
&rust_webview_class,
"setWebContentsDebuggingEnabled",
"(Z)V",
&[devtools.into()],
)?;
if transparent {
set_background_color(&mut self.env, &webview, (0, 0, 0, 0))?;
} else if let Some(color) = background_color {
set_background_color(&mut self.env, &webview, color)?;
}
// Create and set webview client
let client_class_name = format!("{}/RustWebViewClient", PACKAGE.get().unwrap());
let rust_webview_client_class =
find_class(&mut self.env, &activity, client_class_name.clone())?;
let webview_client = self.env.new_object(
&rust_webview_client_class,
format!("(L{webview_class_name};Landroid/content/Context;)V"),
&[(&webview).into(), (&activity).into()],
)?;
self.env.call_method(
&webview,
"setWebViewClient",
"(Landroid/webkit/WebViewClient;)V",
&[(&webview_client).into()],
)?;
// set webchrome client
self.env.call_method(
&webview,
"setWebChromeClient",
"(Landroid/webkit/WebChromeClient;)V",
&[web_chrome_client.as_obj().into()],
)?;
// Add javascript interface (IPC)
let ipc_class = find_class(
&mut self.env,
&activity,
format!("{}/Ipc", PACKAGE.get().unwrap()),
)?;
let ipc = self.env.new_object(
ipc_class,
format!("(L{webview_class_name};L{client_class_name};)V"),
&[(&webview).into(), (&webview_client).into()],
)?;
let ipc_str = self.env.new_string("ipc")?;
self.env.call_method(
&webview,
"addJavascriptInterface",
"(Ljava/lang/Object;Ljava/lang/String;)V",
&[(&ipc).into(), (&ipc_str).into()],
)?;
// Set content view
self.env.call_method(
&activity,
"setContentView",
"(Landroid/view/View;)V",
&[(&webview).into()],
)?;
if let Some(on_webview_created) = on_webview_created {
if let Err(_e) = on_webview_created(super::Context {
env: &mut self.env,
activity: &activity,
webview: &webview,
}) {
#[cfg(feature = "tracing")]
tracing::warn!("failed to run webview created hook: {_e}");
}
}
let webview = self.env.new_global_ref(webview)?;
ACTIVITY_PROXY
.lock()
.unwrap()
.get_mut(&activity_id)
.unwrap()
.webview
.replace(webview);
}
WebViewMessage::Eval(script, callback) => {
if let Some(webview) = get_webview(activity_id) {
let id = EVAL_ID_GENERATOR.next() as i32;
#[cfg(feature = "tracing")]
let span = std::sync::Mutex::new(Some(SendEnteredSpan(
tracing::debug_span!("wry::eval").entered(),
)));
EVAL_CALLBACKS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(
id,
Box::new(move |result| {
#[cfg(feature = "tracing")]
span.lock().unwrap().take();
if let Some(callback) = &callback {
callback(result);
}
}),
);
let s = self.env.new_string(script)?;
self.env.call_method(
webview.as_obj(),
"evalScript",
"(ILjava/lang/String;)V",
&[id.into(), (&s).into()],
)?;
}
}
WebViewMessage::SetBackgroundColor(background_color) => {
if let Some(webview) = get_webview(activity_id) {
set_background_color(&mut self.env, webview.as_obj(), background_color)?;
}
}
WebViewMessage::GetWebViewVersion(tx) => {
if let Some(activity) = activity_proxy(activity_id).map(|p| p.activity.clone()) {
match self
.env
.call_method(activity, "getVersion", "()Ljava/lang/String;", &[])
.and_then(|v| v.l())
.and_then(|s| {
let s = JString::from(s);
self
.env
.get_string(&s)
.map(|v| v.to_string_lossy().to_string())
}) {
Ok(version) => {
tx.send(Ok(version)).unwrap();
}
Err(e) => tx.send(Err(e.into())).unwrap(),
}
} else {
tx.send(Err(Error::ActivityNotFound)).unwrap();
}
}
WebViewMessage::GetUrl(tx) => {
if let Some(webview) = get_webview(activity_id) {
let url = self
.env
.call_method(webview.as_obj(), "getUrl", "()Ljava/lang/String;", &[])
.and_then(|v| v.l())
.and_then(|s| {
let s = JString::from(s);
self
.env
.get_string(&s)
.map(|v| v.to_string_lossy().to_string())
})
.unwrap_or_default();
tx.send(url).unwrap()
}
}
WebViewMessage::Jni(f) => {
match activity_proxy(activity_id).map(|p| (p.activity.clone(), p.webview.clone())) {
Some((activity, Some(webview))) => {
f(&mut self.env, &activity, webview.as_obj());
}
Some((activity, None)) => {
f(&mut self.env, &activity, &JObject::null());
}
_ => {
f(&mut self.env, &JObject::null(), &JObject::null());
}
}
}
WebViewMessage::LoadUrl(url, headers) => {
if let Some(webview) = get_webview(activity_id) {
let url = self.env.new_string(url)?;
load_url(&mut self.env, webview.as_obj(), &url, headers, false)?;
}
}
WebViewMessage::ClearAllBrowsingData => {
if let Some(webview) = get_webview(activity_id) {
self
.env
.call_method(webview, "clearAllBrowsingData", "()V", &[])?;
}
}
WebViewMessage::LoadHtml(html) => {
if let Some(webview) = get_webview(activity_id) {
let html = self.env.new_string(html)?;
load_html(&mut self.env, webview.as_obj(), &html)?;
}
}
WebViewMessage::Reload => {
if let Some(webview) = get_webview(activity_id) {
reload(&mut self.env, webview.as_obj())?;
}
}
WebViewMessage::GetCookies(tx, url) => {
if let Some(webview) = get_webview(activity_id) {
let url = self.env.new_string(url)?;
let cookies = self
.env
.call_method(
webview,
"getCookies",
"(Ljava/lang/String;)Ljava/lang/String;",
&[(&url).into()],
)
.and_then(|v| v.l())
.and_then(|s| {
let s = JString::from(s);
self
.env
.get_string(&s)
.map(|v| v.to_string_lossy().to_string())
})
.unwrap_or_default();
tx.send(
cookies
.split("; ")
.flat_map(|c| cookie::Cookie::parse(c.to_string()))
.collect(),
)
.unwrap();
}
}
WebViewMessage::OnDestroy {
activity_id,
webview_id,
is_changing_configurations,
} => {
// keep our webview references (callbacks etc) alive if the activity is going to be recreated due to configuration changes
// e.g. rotation, multi-window mode change, etc
if !is_changing_configurations {
super::destroy_webview(activity_id, &webview_id);
remove_activity_proxy(activity_id);
}
}
}
}
Ok(())
}
}
fn load_url<'a>(
env: &mut JNIEnv<'a>,
webview: &JObject<'a>,
url: &JString<'a>,
headers: Option<http::HeaderMap>,
main_thread: bool,
) -> JniResult<()> {
let function = if main_thread {
"loadUrlMainThread"
} else {
"loadUrl"
};
if let Some(headers) = headers {
let obj = env.new_object("java/util/HashMap", "()V", &[])?;
let headers_map = {
let headers_map = JMap::from_env(env, &obj)?;
for (name, value) in headers.iter() {
let key = env.new_string(name)?;
let value = env.new_string(value.to_str().unwrap_or_default())?;
headers_map.put(env, &key, &value)?;
}
headers_map
};
env.call_method(
webview,
function,
"(Ljava/lang/String;Ljava/util/Map;)V",
&[url.into(), (&headers_map).into()],
)?;
} else {
env.call_method(webview, function, "(Ljava/lang/String;)V", &[url.into()])?;
}
Ok(())
}
fn load_html<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>, html: &JString<'a>) -> JniResult<()> {
env.call_method(
webview,
"loadHTMLMainThread",
"(Ljava/lang/String;)V",
&[html.into()],
)?;
Ok(())
}
fn reload<'a>(env: &mut JNIEnv<'a>, webview: &JObject<'a>) -> JniResult<()> {
env.call_method(webview, "reload", "()V", &[])?;
Ok(())
}
fn set_background_color<'a>(
env: &mut JNIEnv<'a>,
webview: &JObject<'a>,
(r, g, b, a): RGBA,
) -> JniResult<()> {
let color = (a as i32) << 24 | (r as i32) << 16 | (g as i32) << 8 | (b as i32);
env.call_method(webview, "setBackgroundColor", "(I)V", &[color.into()])?;
Ok(())
}
pub(crate) enum WebViewMessage {
CreateWebView(CreateWebViewAttributes),
Eval(String, Option<EvalCallback>),
SetBackgroundColor(RGBA),
GetWebViewVersion(Sender<Result<String, Error>>),
GetUrl(Sender<String>),
GetCookies(Sender<Vec<cookie::Cookie<'static>>>, String),
Jni(Box<dyn FnOnce(&mut JNIEnv, &JObject, &JObject) + Send>),
LoadUrl(String, Option<http::HeaderMap>),
LoadHtml(String),
Reload,
ClearAllBrowsingData,
OnDestroy {
activity_id: ActivityId,
webview_id: WebviewId,
is_changing_configurations: bool,
},
}
#[derive(Clone)]
pub(crate) struct CreateWebViewAttributes {
pub id: String,
pub url: Option<String>,
pub html: Option<String>,
#[cfg(any(debug_assertions, feature = "devtools"))]
pub devtools: bool,
pub transparent: bool,
pub background_color: Option<RGBA>,
pub headers: Option<http::HeaderMap>,
pub autoplay: bool,
pub on_webview_created:
Option<Arc<dyn Fn(super::Context) -> JniResult<()> + Send + Sync + 'static>>,
pub user_agent: Option<String>,
pub initialization_scripts: Vec<InitializationScript>,
pub javascript_disabled: bool,
}
// SAFETY: only use this when you are sure the span will be dropped on the same thread it was entered
#[cfg(feature = "tracing")]
struct SendEnteredSpan(tracing::span::EnteredSpan);
#[cfg(feature = "tracing")]
unsafe impl Send for SendEnteredSpan {}

530
vendor/wry/src/android/mod.rs vendored Normal file
View File

@ -0,0 +1,530 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::{PageLoadEvent, WebViewAttributes, RGBA};
use crate::{
custom_protocol_workaround, inject_initialization_scripts::inject_scripts_into_html, Error,
RequestAsyncResponder, Result,
};
use crossbeam_channel::*;
use http::{Request, Response as HttpResponse};
use jni::{
errors::Result as JniResult,
objects::{GlobalRef, JClass, JObject},
JNIEnv,
};
use ndk::looper::ThreadLooper;
use once_cell::sync::{Lazy, OnceCell};
use raw_window_handle::HasWindowHandle;
use std::{
borrow::Cow,
collections::HashMap,
sync::{mpsc::channel, Mutex},
time::Duration,
};
pub(crate) mod binding;
mod main_pipe;
use main_pipe::{
activity_id_for_window_manager, first_activity_id, register_activity_proxy, ActivityId,
CreateWebViewAttributes, MainPipe, WebViewMessage,
};
use crate::util::Counter;
static COUNTER: Counter = Counter::new();
const MAIN_PIPE_TIMEOUT: Duration = Duration::from_secs(10);
pub struct Context<'a, 'b> {
pub env: &'a mut JNIEnv<'b>,
pub activity: &'a JObject<'b>,
pub webview: &'a JObject<'b>,
}
type WebviewId = String;
macro_rules! define_static_handlers {
($($key: ident, $var:ident = $type_name:ident);+ $(;)?) => {
$(static $var: Lazy<Mutex<HashMap<$key, $type_name>>> = Lazy::new(||Mutex::new(HashMap::new()));)*
};
($($var:ident = $type_name:ident { $($fields:ident:$types:ty),+ $(,)? });+ $(;)?) => {
$(
static $var: Lazy<Mutex<HashMap<WebviewId, $type_name>>> = Lazy::new(||Mutex::new(HashMap::new()));
pub struct $type_name {
$($fields: $types,)*
}
impl $type_name {
pub fn new($($fields: $types,)*) -> Self {
Self {
$($fields,)*
}
}
}
unsafe impl Send for $type_name {}
unsafe impl Sync for $type_name {})*
};
}
define_static_handlers! {
IPC = UnsafeIpc { handler: Box<dyn Fn(Request<String>)> };
REQUEST_HANDLER = UnsafeRequestHandler { handler: Box<dyn Fn(&str, Request<Vec<u8>>, bool) -> Option<HttpResponse<Cow<'static, [u8]>>>> };
TITLE_CHANGE_HANDLER = UnsafeTitleHandler { handler: Box<dyn Fn(String)> };
URL_LOADING_OVERRIDE = UnsafeUrlLoadingOverride { handler: Box<dyn Fn(String) -> bool> };
ON_LOAD_HANDLER = UnsafeOnPageLoadHandler { handler: Box<dyn Fn(PageLoadEvent, String)> };
}
define_static_handlers! {
WebviewId, WITH_ASSET_LOADER = bool;
WebviewId, ASSET_LOADER_DOMAIN = String;
ActivityId, WEBVIEW_ATTRIBUTES = CreateWebViewAttributes;
}
pub(crate) static PACKAGE: OnceCell<String> = OnceCell::new();
type EvalCallback = Box<dyn Fn(String) + Send + 'static>;
pub static EVAL_ID_GENERATOR: Counter = Counter::new();
pub static EVAL_CALLBACKS: OnceCell<Mutex<HashMap<i32, EvalCallback>>> = OnceCell::new();
pub fn destroy_webview(activity_id: ActivityId, webview_id: &WebviewId) {
WEBVIEW_ATTRIBUTES.lock().unwrap().remove(&activity_id);
IPC.lock().unwrap().remove(webview_id);
REQUEST_HANDLER.lock().unwrap().remove(webview_id);
TITLE_CHANGE_HANDLER.lock().unwrap().remove(webview_id);
URL_LOADING_OVERRIDE.lock().unwrap().remove(webview_id);
ON_LOAD_HANDLER.lock().unwrap().remove(webview_id);
WITH_ASSET_LOADER.lock().unwrap().remove(webview_id);
ASSET_LOADER_DOMAIN.lock().unwrap().remove(webview_id);
}
/// Sets up the necessary logic for wry to be able to create the webviews later.
///
/// This function must be run on the thread where the [`JNIEnv`] is registered and the looper is local,
/// hence the requirement for a [`ThreadLooper`].
pub unsafe fn android_setup(
package: &str,
mut env: JNIEnv,
_looper: &ThreadLooper,
activity: GlobalRef,
) {
PACKAGE.get_or_init(move || package.to_string());
let vm = env.get_java_vm().unwrap();
let activity_id = env
.call_method(activity.as_obj(), "getId", "()I", &[])
.unwrap()
.i()
.unwrap();
let window_manager = env
.call_method(
&activity,
"getWindowManager",
"()Landroid/view/WindowManager;",
&[],
)
.unwrap()
.l()
.unwrap();
let window_manager = env.new_global_ref(window_manager).unwrap();
// we must create the WebChromeClient here because it calls `registerForActivityResult`,
// which gives an `LifecycleOwners must call register before they are STARTED.` error when called outside the onCreate hook
let rust_webchrome_client_class = find_class(
&mut env,
activity.as_obj(),
format!("{}/RustWebChromeClient", PACKAGE.get().unwrap()),
)
.unwrap();
let webchrome_client = env
.new_object(
&rust_webchrome_client_class,
format!("(L{}/WryActivity;)V", PACKAGE.get().unwrap()),
&[activity.as_obj().into()],
)
.unwrap();
let webchrome_client = env.new_global_ref(webchrome_client).unwrap();
register_activity_proxy(vm, activity_id, activity, window_manager, webchrome_client);
if let Some(webview_attributes) = WEBVIEW_ATTRIBUTES.lock().unwrap().get(&activity_id) {
MainPipe::send(
activity_id,
WebViewMessage::CreateWebView(webview_attributes.clone()),
);
}
}
pub(crate) struct InnerWebView {
id: String,
pub activity_id: ActivityId,
}
impl InnerWebView {
pub fn new_as_child(
window: &impl HasWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
) -> Result<Self> {
Self::new(window, attributes, pl_attrs)
}
pub fn new(
window: &impl HasWindowHandle,
attributes: WebViewAttributes,
pl_attrs: super::PlatformSpecificWebViewAttributes,
) -> Result<Self> {
let window_manager = match window.window_handle()?.as_raw() {
raw_window_handle::RawWindowHandle::AndroidNdk(window_manager) => {
window_manager.a_native_window
}
_ => return Err(Error::UnsupportedWindowHandle),
};
let window_manager = unsafe { JObject::from_raw(window_manager.as_ptr().cast()) };
let activity_id =
activity_id_for_window_manager(window_manager).expect("no available activity");
let WebViewAttributes {
url,
html,
initialization_scripts,
ipc_handler,
#[cfg(any(debug_assertions, feature = "devtools"))]
devtools,
custom_protocols,
background_color,
transparent,
headers,
autoplay,
user_agent,
javascript_disabled,
..
} = attributes;
let super::PlatformSpecificWebViewAttributes {
on_webview_created,
with_asset_loader,
asset_loader_domain,
https_scheme,
} = pl_attrs;
let http_or_https = if https_scheme { "https" } else { "http" };
let url = if let Some(mut url) = url {
if let Some((protocol, _)) = url.split_once("://") {
if custom_protocols.contains_key(protocol) {
url = custom_protocol_workaround::apply_uri_work_around(&url, http_or_https, protocol)
}
}
Some(url)
} else {
None
};
let id = attributes
.id
.map(|id| id.to_string())
.unwrap_or_else(|| COUNTER.next().to_string());
WITH_ASSET_LOADER
.lock()
.unwrap()
.insert(id.clone(), with_asset_loader);
if let Some(domain) = asset_loader_domain {
ASSET_LOADER_DOMAIN
.lock()
.unwrap()
.insert(id.clone(), domain);
}
let initialization_scripts_ = initialization_scripts.clone();
REQUEST_HANDLER
.lock()
.unwrap()
.insert(
id.clone(),
UnsafeRequestHandler::new(Box::new(
move |webview_id: &str, mut request, is_document_start_script_enabled| {
let uri = request.uri().to_string();
if let Some((custom_protocol, custom_protocol_handler)) =
custom_protocols.iter().find(|(protocol, _)| {
custom_protocol_workaround::is_work_around_uri(&uri, http_or_https, protocol)
})
{
let uri_res = custom_protocol_workaround::revert_uri_work_around(
&uri,
http_or_https,
custom_protocol,
)
.parse();
if let Ok(uri) = uri_res {
*request.uri_mut() = uri;
}
let (tx, rx) = channel();
let initialization_scripts = initialization_scripts_.clone();
let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
Box::new(move |mut response| {
if !is_document_start_script_enabled {
#[cfg(feature = "tracing")]
tracing::info!("`addDocumentStartJavaScript` is not supported; injecting initialization scripts via custom protocol handler");
response = inject_scripts_into_html(response, &initialization_scripts);
}
let _ = tx.send(response);
});
(custom_protocol_handler)(webview_id, request, RequestAsyncResponder { responder });
// 3x the timeout while we monitor https://github.com/tauri-apps/wry/issues/1551
// TODO: Remove timeout
return rx.recv_timeout(MAIN_PIPE_TIMEOUT * 3).inspect_err(|e| {eprintln!("custom protocol timed out: {e}");}).ok();
}
None
},
)));
if let Some(i) = ipc_handler {
IPC
.lock()
.unwrap()
.insert(id.clone(), UnsafeIpc::new(Box::new(i)));
}
if let Some(i) = attributes.document_title_changed_handler {
TITLE_CHANGE_HANDLER
.lock()
.unwrap()
.insert(id.clone(), UnsafeTitleHandler::new(i));
}
if let Some(i) = attributes.navigation_handler {
URL_LOADING_OVERRIDE
.lock()
.unwrap()
.insert(id.clone(), UnsafeUrlLoadingOverride::new(i));
}
if let Some(h) = attributes.on_page_load_handler {
ON_LOAD_HANDLER
.lock()
.unwrap()
.insert(id.clone(), UnsafeOnPageLoadHandler::new(h));
}
let attributes = CreateWebViewAttributes {
id: id.clone(),
url,
html,
#[cfg(any(debug_assertions, feature = "devtools"))]
devtools,
background_color,
transparent,
headers,
on_webview_created,
autoplay,
user_agent,
initialization_scripts,
javascript_disabled,
};
WEBVIEW_ATTRIBUTES
.lock()
.unwrap()
.insert(activity_id, attributes.clone());
MainPipe::send(activity_id, WebViewMessage::CreateWebView(attributes));
Ok(Self { id, activity_id })
}
pub fn print(&self) -> crate::Result<()> {
Ok(())
}
pub fn id(&self) -> crate::WebViewId<'_> {
&self.id
}
pub fn url(&self) -> crate::Result<String> {
let (tx, rx) = bounded(1);
MainPipe::send(self.activity_id, WebViewMessage::GetUrl(tx));
rx.recv_timeout(MAIN_PIPE_TIMEOUT).map_err(Into::into)
}
pub fn eval(&self, js: &str, callback: Option<impl Fn(String) + Send + 'static>) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::Eval(
js.into(),
callback.map(|c| Box::new(c) as Box<dyn Fn(String) + Send + 'static>),
),
);
Ok(())
}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn open_devtools(&self) {}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn close_devtools(&self) {}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn is_devtools_open(&self) -> bool {
false
}
pub fn zoom(&self, _scale_factor: f64) -> Result<()> {
Ok(())
}
pub fn set_background_color(&self, background_color: RGBA) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::SetBackgroundColor(background_color),
);
Ok(())
}
pub fn load_url(&self, url: &str) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::LoadUrl(url.to_string(), None),
);
Ok(())
}
pub fn load_url_with_headers(&self, url: &str, headers: http::HeaderMap) -> Result<()> {
MainPipe::send(
self.activity_id,
WebViewMessage::LoadUrl(url.to_string(), Some(headers)),
);
Ok(())
}
pub fn load_html(&self, html: &str) -> Result<()> {
MainPipe::send(self.activity_id, WebViewMessage::LoadHtml(html.to_string()));
Ok(())
}
pub fn reload(&self) -> Result<()> {
MainPipe::send(self.activity_id, WebViewMessage::Reload);
Ok(())
}
pub fn clear_all_browsing_data(&self) -> Result<()> {
MainPipe::send(self.activity_id, WebViewMessage::ClearAllBrowsingData);
Ok(())
}
pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
let (tx, rx) = bounded(1);
MainPipe::send(
self.activity_id,
WebViewMessage::GetCookies(tx, url.to_string()),
);
rx.recv_timeout(MAIN_PIPE_TIMEOUT).map_err(Into::into)
}
pub fn set_cookie(&self, #[allow(unused)] cookie: &cookie::Cookie<'_>) -> Result<()> {
// Unsupported
Ok(())
}
pub fn delete_cookie(&self, #[allow(unused)] cookie: &cookie::Cookie<'_>) -> Result<()> {
// Unsupported
Ok(())
}
pub fn cookies(&self) -> Result<Vec<cookie::Cookie<'static>>> {
Ok(Vec::new())
}
pub fn bounds(&self) -> Result<crate::Rect> {
Ok(crate::Rect::default())
}
pub fn set_bounds(&self, _bounds: crate::Rect) -> Result<()> {
// Unsupported
Ok(())
}
pub fn set_visible(&self, _visible: bool) -> Result<()> {
// Unsupported
Ok(())
}
pub fn focus(&self) -> Result<()> {
// Unsupported
Ok(())
}
pub fn focus_parent(&self) -> Result<()> {
// Unsupported
Ok(())
}
}
#[derive(Clone, Copy)]
pub struct JniHandle {
pub(crate) activity_id: ActivityId,
}
impl JniHandle {
/// Execute jni code on the thread of the webview.
/// Provided function will be provided with the jni evironment, Android activity and WebView
pub fn exec<F>(&self, func: F)
where
F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static,
{
MainPipe::send(self.activity_id, WebViewMessage::Jni(Box::new(func)));
}
}
pub fn platform_webview_version() -> Result<String> {
let (tx, rx) = bounded(1);
let activity_id = loop {
match first_activity_id() {
Some(id) => break id,
None => {
std::thread::sleep(Duration::from_millis(100));
}
}
};
MainPipe::send(activity_id, WebViewMessage::GetWebViewVersion(tx));
rx.recv_timeout(MAIN_PIPE_TIMEOUT)?
}
/// Finds a class in the project scope.
pub fn find_class<'a>(
env: &mut JNIEnv<'a>,
activity: &JObject<'_>,
name: String,
) -> JniResult<JClass<'a>> {
let class_name = env.new_string(name.replace('/', "."))?;
let my_class = env
.call_method(
activity,
"getAppClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[(&class_name).into()],
)?
.l()?;
Ok(my_class.into())
}
/// Dispatch a closure to run on the Android context.
///
/// The closure takes the JNI env, the Android activity instance and the possibly null webview.
pub fn dispatch<F>(func: F)
where
F: FnOnce(&mut JNIEnv, &JObject, &JObject) + Send + 'static,
{
MainPipe::send(
first_activity_id().expect("no available activity"),
WebViewMessage::Jni(Box::new(func)),
);
}

View File

@ -0,0 +1,56 @@
//! - WebView2 supports non-standard protocols only on Windows 10+, so we have to use a workaround.
//! See <https://github.com/MicrosoftEdge/WebView2Feedback/issues/73>
//! - On Android, there's no API for registering custom protocols, so this workaround is also used.
//!
//! The process looks like this:
//!
//! 1. Use [`apply_uri_work_around`] to convert the URI we want to navigate to
//! 2. Intercept http(s) requests, test the request URI against [`is_work_around_uri`],
//! if it matches, we apply [`revert_uri_work_around`] to the URI and feed it to the custom protocol handler
/// If the URI is a work around URI for this protocol which starts with `{http_or_https}://{protocol}.`
pub fn is_work_around_uri(uri: &str, http_or_https: &str, protocol: &str) -> bool {
uri
.strip_prefix(http_or_https)
.and_then(|rest| rest.strip_prefix("://"))
.and_then(|rest| rest.strip_prefix(protocol))
.and_then(|rest| rest.strip_prefix("."))
.is_some()
}
/// Conveting `{protocol}://localhost/abc` to `{http_or_https}://{protocol}.localhost/abc`
pub fn apply_uri_work_around(uri: &str, http_or_https: &str, protocol: &str) -> String {
uri.replace(
&original_uri_prefix(protocol),
&work_around_uri_prefix(http_or_https, protocol),
)
}
/// Conveting `{http_or_https}://{protocol}.localhost/abc` back to `{protocol}://localhost/abc`
pub fn revert_uri_work_around(uri: &str, http_or_https: &str, protocol: &str) -> String {
uri.replace(
&work_around_uri_prefix(http_or_https, protocol),
&original_uri_prefix(protocol),
)
}
pub fn original_uri_prefix(protocol: &str) -> String {
format!("{protocol}://")
}
pub fn work_around_uri_prefix(http_or_https: &str, protocol: &str) -> String {
format!("{http_or_https}://{protocol}.")
}
#[cfg(test)]
mod tests {
use super::is_work_around_uri;
#[test]
fn checks_if_custom_protocol_uri() {
let scheme = "http";
let uri = "http://wry.localhost/path/to/page";
assert!(is_work_around_uri(uri, scheme, "wry"));
assert!(!is_work_around_uri(uri, scheme, "asset"));
}
}

80
vendor/wry/src/error.rs vendored Normal file
View File

@ -0,0 +1,80 @@
/// Convenient type alias of Result type for wry.
pub type Result<T> = std::result::Result<T, Error>;
/// Errors returned by wry.
#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[cfg(gtk)]
#[error(transparent)]
GlibError(#[from] gtk::glib::Error),
#[cfg(gtk)]
#[error(transparent)]
GlibBoolError(#[from] gtk::glib::BoolError),
#[cfg(gtk)]
#[error("Fail to fetch security manager")]
MissingManager,
#[cfg(gtk)]
#[error("Couldn't find X11 Display")]
X11DisplayNotFound,
#[cfg(all(gtk, feature = "x11"))]
#[error(transparent)]
XlibError(#[from] x11_dl::error::OpenError),
#[error("Failed to initialize the script")]
InitScriptError,
#[error("Bad RPC request: {0} ((1))")]
RpcScriptError(String, String),
#[error(transparent)]
NulError(#[from] std::ffi::NulError),
#[error(transparent)]
ReceiverError(#[from] std::sync::mpsc::RecvError),
#[cfg(target_os = "android")]
#[error(transparent)]
ReceiverTimeoutError(#[from] crossbeam_channel::RecvTimeoutError),
#[error(transparent)]
SenderError(#[from] std::sync::mpsc::SendError<String>),
#[error("Failed to send the message")]
MessageSender,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[cfg(target_os = "windows")]
#[error("WebView2 error: {0}")]
WebView2Error(webview2_com::Error),
#[error(transparent)]
HttpError(#[from] http::Error),
#[error("Infallible error, something went really wrong: {0}")]
Infallible(#[from] std::convert::Infallible),
#[cfg(target_os = "android")]
#[error(transparent)]
JniError(#[from] jni::errors::Error),
#[error("Failed to create proxy endpoint")]
ProxyEndpointCreationFailed,
#[error(transparent)]
WindowHandleError(#[from] raw_window_handle::HandleError),
#[error("the window handle kind is not supported")]
UnsupportedWindowHandle,
#[error(transparent)]
Utf8Error(#[from] std::str::Utf8Error),
#[cfg(target_os = "android")]
#[error(transparent)]
CrossBeamRecvError(#[from] crossbeam_channel::RecvError),
#[error("not on the main thread")]
NotMainThread,
#[error("Custom protocol task is invalid.")]
CustomProtocolTaskInvalid,
#[error("Failed to register URL scheme: {0}, could be due to invalid URL scheme or the scheme is already registered.")]
UrlSchemeRegisterError(String),
#[error("Duplicate custom protocol '{0}' registered on the WebViewBuilder")]
DuplicateCustomProtocol(String),
#[error("Duplicate custom protocol '{0}' registered on the same web context on Linux")]
ContextDuplicateCustomProtocol(String),
#[error(transparent)]
#[cfg(any(target_os = "macos", target_os = "ios"))]
UrlParse(#[from] url::ParseError),
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[error("data store is currently opened")]
DataStoreInUse,
#[cfg(target_os = "android")]
#[error("Activity not found")]
ActivityNotFound,
}

View File

@ -0,0 +1,225 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! This is an internal implementation detail used by the Android backend to inject
//! initialization scripts when `addDocumentStartJavaScript` is not supported.
use base64::{prelude::BASE64_STANDARD, Engine};
use dom_query::Document;
use http::{
header::{HeaderValue, CONTENT_SECURITY_POLICY, CONTENT_TYPE},
Response as HttpResponse,
};
use sha2::{Digest, Sha256};
use std::borrow::Cow;
use crate::InitializationScript;
pub fn inject_scripts_into_html(
mut response: HttpResponse<Cow<'static, [u8]>>,
scripts: &[InitializationScript],
) -> HttpResponse<Cow<'static, [u8]>> {
if scripts.is_empty() {
return response;
}
let should_inject_scripts = response
.headers()
.get(CONTENT_TYPE)
// Content-Type must begin with the media type, but is case-insensitive.
// It may also be followed by any number of semicolon-delimited key value pairs.
// We don't care about these here.
// source: https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.1
.and_then(|content_type| content_type.to_str().ok())
.map(|content_type_str| content_type_str.to_lowercase().starts_with("text/html"))
.unwrap_or_default();
if !should_inject_scripts {
return response;
}
let document = Document::from(String::from_utf8_lossy(response.body()).as_ref());
let csp = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY);
// Get or create head element
let head = document.head().unwrap_or_else(|| {
let html = document.html_root();
let head = document.tree.new_element("head");
html.prepend_child(&head);
head
});
// Iterate in reverse order since we are prepending each script to the head tag
let mut hashes = Vec::new();
for script in scripts.iter().rev().map(|s| &s.script) {
let script_tag = document.tree.new_element("script");
script_tag.set_text(script.as_str());
head.prepend_child(&script_tag);
if csp.is_some() {
hashes.push(hash_script(script));
}
}
if let Some(csp) = csp {
let csp_string = csp.to_str().unwrap().to_string();
let csp_string = if csp_string.contains("script-src") {
csp_string.replace("script-src", &format!("script-src {}", hashes.join(" ")))
} else {
format!("{csp_string} script-src {}", hashes.join(" "))
};
*csp = HeaderValue::from_str(&csp_string).unwrap();
}
*response.body_mut() = Cow::Owned(document.html().as_bytes().to_vec());
response
}
fn hash_script(script: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(script);
let hash = hasher.finalize();
format!("'sha256-{}'", BASE64_STANDARD.encode(hash))
}
#[cfg(test)]
mod tests {
use super::*;
use http::StatusCode;
#[test]
fn test_no_scripts_returns_original_response() {
let body = "<html><head></head><body>Test</body></html>";
let result = run(body, "text/html", vec![]);
assert_eq!(result, body);
}
#[test]
fn test_non_html_response_not_modified() {
let body = r#"{"key": "value"}"#;
let scripts = vec!["console.log('test');".to_string()];
let result = run(body, "application/json", scripts);
assert_eq!(result, body);
}
#[test]
fn test_inject_single_script() {
let body = "<html><head></head><body>Content</body></html>";
let scripts = vec!["console.log('injected');".to_string()];
let result = run(body, "text/html", scripts);
assert_eq!(
result,
"<html><head><script>console.log('injected');</script></head><body>Content</body></html>"
);
}
#[test]
fn test_inject_multiple_scripts() {
let body = "<html><head></head><body>Content</body></html>";
let scripts = vec![
"var first = 1;".to_owned(),
"let second = 2;".to_owned(),
"const third = 3;".to_owned(),
"window.test = () => console.log('test');".to_owned(),
];
let result = run(body, "text/html", scripts);
assert_eq!(
result,
"<html><head><script>var first = 1;</script><script>let second = 2;</script><script>const third = 3;</script><script>window.test = () => console.log('test');</script></head><body>Content</body></html>"
);
}
#[test]
fn test_inject_script_creates_head_if_missing() {
let body = "<html><body>Content</body></html>";
let scripts = vec!["console.log('test');".to_string()];
let result = run(body, "text/html", scripts);
assert_eq!(
result,
"<html><head><script>console.log('test');</script></head><body>Content</body></html>"
);
}
#[test]
fn test_inject_script_creates_html_structure_if_missing() {
let body = "Just some text";
let scripts = vec!["console.log('test');".to_string()];
let result = run(body, "text/html", scripts);
assert_eq!(
result,
"<html><head><script>console.log('test');</script></head><body>Just some text</body></html>"
);
}
#[test]
fn test_csp_header_updated_with_script_hashes() {
let body = "<html><head></head><body>Content</body></html>";
let mut response = create_response(body, "text/html");
response.headers_mut().insert(
CONTENT_SECURITY_POLICY,
HeaderValue::from_static("default-src 'self'"),
);
let script_code = "console.log('test');";
let scripts = vec![script_code.to_string()];
let scripts: Vec<InitializationScript> = scripts
.into_iter()
.map(|script| InitializationScript {
script,
for_main_frame_only: true,
})
.collect();
let result = inject_scripts_into_html(response, &scripts);
let result_body = String::from_utf8_lossy(result.body()).to_string();
let csp = result.headers().get(CONTENT_SECURITY_POLICY).unwrap();
let csp_str = csp.to_str().unwrap();
assert_eq!(
result_body,
"<html><head><script>console.log('test');</script></head><body>Content</body></html>"
);
assert_eq!(
csp_str,
"default-src 'self' script-src 'sha256-3x8DE279hr8o/Aq0dEdH4WApIwn5rbRKhugPzn6Bofw='"
);
}
fn create_response(body: &str, content_type: &'static str) -> HttpResponse<Cow<'static, [u8]>> {
let mut response = HttpResponse::builder()
.status(StatusCode::OK)
.body(Cow::Owned(body.as_bytes().to_vec()))
.unwrap();
response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
response
}
/// Helper function to create a response, inject scripts, and return the body as a string
fn run(body: &str, content_type: &'static str, scripts: Vec<String>) -> String {
let response = create_response(body, content_type);
let scripts: Vec<InitializationScript> = scripts
.into_iter()
.map(|script| InitializationScript {
script,
for_main_frame_only: true,
})
.collect();
let result = inject_scripts_into_html(response, &scripts);
String::from_utf8_lossy(result.body()).to_string()
}
}

2514
vendor/wry/src/lib.rs vendored Normal file

File diff suppressed because it is too large Load Diff

15
vendor/wry/src/proxy.rs vendored Normal file
View File

@ -0,0 +1,15 @@
#[derive(Debug, Clone)]
pub struct ProxyEndpoint {
/// Proxy server host (e.g. 192.168.0.100, localhost, example.com, etc.)
pub host: String,
/// Proxy server port (e.g. 1080, 3128, etc.)
pub port: String,
}
#[derive(Debug, Clone)]
pub enum ProxyConfig {
/// Connect to proxy server via HTTP CONNECT
Http(ProxyEndpoint),
/// Connect to proxy server via SOCKSv5
Socks5(ProxyEndpoint),
}

13
vendor/wry/src/util.rs vendored Normal file
View File

@ -0,0 +1,13 @@
use std::sync::atomic::{AtomicU32, Ordering};
pub struct Counter(AtomicU32);
impl Counter {
pub const fn new() -> Self {
Self(AtomicU32::new(1))
}
pub fn next(&self) -> u32 {
self.0.fetch_add(1, Ordering::Relaxed)
}
}

113
vendor/wry/src/web_context.rs vendored Normal file
View File

@ -0,0 +1,113 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(gtk)]
use crate::webkitgtk::WebContextImpl;
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
/// A context that is shared between multiple [`WebView`]s.
///
/// A browser would have a context for all the normal tabs and a different context for all the
/// private/incognito tabs.
///
/// # Warning
///
/// If [`WebView`] is created by a WebContext. Dropping `WebContext` will cause [`WebView`] lose
/// some actions like custom protocol on Mac. Please keep both instances when you still wish to
/// interact with them.
///
/// [`WebView`]: crate::WebView
#[derive(Debug)]
pub struct WebContext {
data_directory: Option<PathBuf>,
#[allow(dead_code)] // It's not needed on Windows and macOS.
pub(crate) os: WebContextImpl,
#[allow(dead_code)] // It's not needed on Windows and macOS.
pub(crate) custom_protocols: HashSet<String>,
}
impl WebContext {
/// Create a new [`WebContext`].
///
/// - `data_directory`: Whether the WebView window should have a custom user data path.
/// This is useful in Windows when a bundled application can't have the webview data inside `Program Files`.
///
/// ## Platform-specific:
///
/// - **Windows**: Webview instances with different `CoreWebView2EnvironmentOptions` must have different `data_directory`s [^1]
///
/// [^1]: <https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2environment.createcorewebview2controllerasync?view=webview2-dotnet-1.0.3719.77#:~:text=WebView%20creation%20fails%20if%20a%20running%20instance%20using%20the%20same%20user%20data%20folder%20exists%2C%20and%20the%20Environment%20objects%20have%20different%20CoreWebView2EnvironmentOptions.>
pub fn new(data_directory: Option<PathBuf>) -> Self {
Self {
os: WebContextImpl::new(data_directory.as_deref()),
data_directory,
custom_protocols: Default::default(),
}
}
#[cfg(gtk)]
pub(crate) fn new_ephemeral() -> Self {
Self {
os: WebContextImpl::new_ephemeral(),
data_directory: None,
custom_protocols: Default::default(),
}
}
/// A reference to the data directory the context was created with.
pub fn data_directory(&self) -> Option<&Path> {
self.data_directory.as_deref()
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
pub(crate) fn register_custom_protocol(&mut self, name: String) -> Result<(), crate::Error> {
if self.is_custom_protocol_registered(&name) {
return Err(crate::Error::ContextDuplicateCustomProtocol(name));
}
self.custom_protocols.insert(name);
Ok(())
}
/// Check if a custom protocol has been registered on this context.
pub fn is_custom_protocol_registered(&self, name: &str) -> bool {
self.custom_protocols.contains(name)
}
/// Set if this context allows automation.
///
/// **Note:** This is currently only enforced on Linux, and has the stipulation that
/// only 1 context allows automation at a time.
pub fn set_allows_automation(&mut self, flag: bool) {
self.os.set_allows_automation(flag);
}
}
impl Default for WebContext {
fn default() -> Self {
Self::new(None)
}
}
#[cfg(not(gtk))]
#[derive(Debug)]
pub(crate) struct WebContextImpl;
#[cfg(not(gtk))]
impl WebContextImpl {
fn new(_: Option<&Path>) -> Self {
Self
}
fn set_allows_automation(&mut self, _flag: bool) {}
}

143
vendor/wry/src/webkitgtk/drag_drop.rs vendored Normal file
View File

@ -0,0 +1,143 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
cell::{Cell, UnsafeCell},
path::PathBuf,
rc::Rc,
};
use gtk::{glib::GString, prelude::*};
use webkit2gtk::WebView;
use crate::DragDropEvent;
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
enum DragControllerState {
Entered,
Leaving,
Left,
}
struct DragDropController {
paths: UnsafeCell<Option<Vec<PathBuf>>>,
state: Cell<DragControllerState>,
position: Cell<(i32, i32)>,
handler: Box<dyn Fn(DragDropEvent) -> bool>,
}
impl DragDropController {
fn new(handler: Box<dyn Fn(DragDropEvent) -> bool>) -> Self {
Self {
handler,
paths: UnsafeCell::new(None),
state: Cell::new(DragControllerState::Left),
position: Cell::new((0, 0)),
}
}
fn store_paths(&self, paths: Vec<PathBuf>) {
unsafe { *self.paths.get() = Some(paths) };
}
fn take_paths(&self) -> Option<Vec<PathBuf>> {
unsafe { &mut *self.paths.get() }.take()
}
fn store_position(&self, position: (i32, i32)) {
self.position.replace(position);
}
fn enter(&self) {
self.state.set(DragControllerState::Entered);
}
fn leaving(&self) {
self.state.set(DragControllerState::Leaving);
}
fn leave(&self) {
self.state.set(DragControllerState::Left);
}
fn state(&self) -> DragControllerState {
self.state.get()
}
fn call(&self, event: DragDropEvent) -> bool {
(self.handler)(event)
}
}
pub(crate) fn connect_drag_event(webview: &WebView, handler: Box<dyn Fn(DragDropEvent) -> bool>) {
let controller = Rc::new(DragDropController::new(handler));
{
let controller = controller.clone();
webview.connect_drag_data_received(move |_, _, _, _, data, info, _| {
if info == 2 {
let uris = data.uris();
let paths = uris.iter().map(path_buf_from_uri).collect::<Vec<_>>();
controller.enter();
controller.call(DragDropEvent::Enter {
paths: paths.clone(),
position: controller.position.get(),
});
controller.store_paths(paths);
}
});
}
{
let controller = controller.clone();
webview.connect_drag_motion(move |_, _, x, y, _| {
if controller.state() == DragControllerState::Entered {
controller.call(DragDropEvent::Over { position: (x, y) });
} else {
controller.store_position((x, y));
}
false
});
}
{
let controller = controller.clone();
webview.connect_drag_drop(move |_, ctx, x, y, time| {
if controller.state() == DragControllerState::Leaving {
if let Some(paths) = controller.take_paths() {
ctx.drop_finish(true, time);
controller.leave();
return controller.call(DragDropEvent::Drop {
paths,
position: (x, y),
});
}
}
false
});
}
webview.connect_drag_leave(move |_w, _, _| {
if controller.state() != DragControllerState::Left {
controller.leaving();
let controller = controller.clone();
gtk::glib::idle_add_local_once(move || {
if controller.state() == DragControllerState::Leaving {
controller.leave();
controller.call(DragDropEvent::Leave);
}
});
}
});
}
fn path_buf_from_uri(gstr: &GString) -> PathBuf {
let path = gstr.as_str();
let path = path.strip_prefix("file://").unwrap_or(path);
let path = percent_encoding::percent_decode(path.as_bytes())
.decode_utf8_lossy()
.to_string();
PathBuf::from(path)
}

1249
vendor/wry/src/webkitgtk/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
use std::{cell::RefCell, rc::Rc};
use gtk::{
gdk::{EventButton, EventMask, ModifierType},
prelude::*,
};
use webkit2gtk::{WebView, WebViewExt};
pub fn setup(webview: &WebView) {
webview.add_events(EventMask::BUTTON1_MOTION_MASK | EventMask::BUTTON_PRESS_MASK);
let bf_state = BackForwardState(Rc::new(RefCell::new(0)));
let bf_state_c = bf_state.clone();
webview.connect_button_press_event(move |webview, event| {
let mut inhibit = false;
match event.button() {
// back button
8 => {
inhibit = true;
bf_state_c.set(BACK);
webview.run_javascript(
&create_js_mouse_event(event, true, &bf_state_c),
None::<&gtk::gio::Cancellable>,
|_| {},
);
}
// forward button
9 => {
inhibit = true;
bf_state_c.set(FORWARD);
webview.run_javascript(
&create_js_mouse_event(event, true, &bf_state_c),
None::<&gtk::gio::Cancellable>,
|_| {},
);
}
_ => {}
}
if inhibit {
gtk::glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}
});
let bf_state_c = bf_state.clone();
webview.connect_button_release_event(move |webview, event| {
let mut inhibit = false;
match event.button() {
// back button
8 => {
inhibit = true;
bf_state_c.remove(BACK);
webview.run_javascript(
&create_js_mouse_event(event, false, &bf_state_c),
None::<&gtk::gio::Cancellable>,
|_| {},
);
}
// forward button
9 => {
inhibit = true;
bf_state_c.remove(FORWARD);
webview.run_javascript(
&create_js_mouse_event(event, false, &bf_state_c),
None::<&gtk::gio::Cancellable>,
|_| {},
);
}
_ => {}
}
if inhibit {
gtk::glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}
});
}
fn create_js_mouse_event(event: &EventButton, pressed: bool, state: &BackForwardState) -> String {
let event_name = if pressed { "mousedown" } else { "mouseup" };
// js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
let button = if event.button() == 8 { 3 } else { 4 };
let (x, y) = event.position();
let (x, y) = (x as i32, y as i32);
let modifers_state = event.state();
let mut buttons = 0;
// left button
if modifers_state.contains(ModifierType::BUTTON1_MASK) {
buttons += 1;
}
// right button
if modifers_state.contains(ModifierType::BUTTON3_MASK) {
buttons += 2;
}
// middle button
if modifers_state.contains(ModifierType::BUTTON2_MASK) {
buttons += 4;
}
// back button
if state.has(BACK) {
buttons += 8;
}
// if modifers_state.contains(ModifierType::BUTTON4_MASK) {
// buttons += 8;
// }
// forward button
if state.has(FORWARD) {
buttons += 16;
}
// if modifers_state.contains(ModifierType::BUTTON5_MASK) {
// buttons += 16;
// }
format!(
r#"(() => {{
const el = document.elementFromPoint({x},{y});
const ev = new MouseEvent('{event_name}', {{
view: window,
button: {button},
buttons: {buttons},
x: {x},
y: {y},
bubbles: true,
detail: {detail},
cancelBubble: false,
cancelable: true,
clientX: {x},
clientY: {y},
composed: true,
layerX: {x},
layerY: {y},
pageX: {x},
pageY: {y},
screenX: window.screenX + {x},
screenY: window.screenY + {y},
ctrlKey: {ctrl_key},
metaKey: {meta_key},
shiftKey: {shift_key},
altKey: {alt_key},
}});
el.dispatchEvent(ev)
if (!ev.defaultPrevented && "{event_name}" === "mouseup") {{
if (ev.button === 3) {{
window.history.back();
}}
if (ev.button === 4) {{
window.history.forward();
}}
}}
}})()"#,
event_name = event_name,
x = x,
y = y,
detail = event.click_count().unwrap_or(1),
ctrl_key = modifers_state.contains(ModifierType::CONTROL_MASK),
alt_key = modifers_state.contains(ModifierType::MOD1_MASK),
shift_key = modifers_state.contains(ModifierType::SHIFT_MASK),
meta_key = modifers_state.contains(ModifierType::SUPER_MASK),
button = button,
buttons = buttons,
)
}
// Internal modifiers to track whether BACK/FORWARD buttons are pressed
const BACK: u8 = 0b01;
const FORWARD: u8 = 0b10;
/// A single u8 that stores whether [BACK] and [FORWARD] are pressed or not
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct BackForwardState(Rc<RefCell<u8>>);
impl BackForwardState {
fn set(&self, button: u8) {
*self.0.borrow_mut() |= button
}
fn remove(&self, button: u8) {
*self.0.borrow_mut() &= !button
}
fn has(&self, button: u8) -> bool {
let state = *self.0.borrow();
state & !button != state
}
}

404
vendor/wry/src/webkitgtk/web_context.rs vendored Normal file
View File

@ -0,0 +1,404 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Unix platform extensions for [`WebContext`](super::WebContext).
use crate::{Error, RequestAsyncResponder};
use gtk::glib::{self, MainContext, ObjectExt};
use http::{header::CONTENT_TYPE, HeaderName, HeaderValue, Request, Response as HttpResponse};
use soup::{MessageHeaders, MessageHeadersType};
use std::{
borrow::Cow,
cell::RefCell,
env::current_dir,
path::{Path, PathBuf},
rc::Rc,
};
use webkit2gtk::{
ApplicationInfo, AutomationSessionExt, CookiePersistentStorage, DownloadExt, SecurityManagerExt,
URIRequest, URIRequestExt, URISchemeRequest, URISchemeRequestExt, URISchemeResponse,
URISchemeResponseExt, WebContext, WebContextExt as Webkit2gtkContextExt, WebView, WebViewExt,
};
#[derive(Debug)]
pub struct WebContextImpl {
context: WebContext,
automation: bool,
app_info: Option<ApplicationInfo>,
}
impl WebContextImpl {
pub fn new(data_directory: Option<&Path>) -> Self {
use webkit2gtk::{CookieManagerExt, WebsiteDataManager, WebsiteDataManagerExt};
let mut context_builder = WebContext::builder();
if let Some(data_directory) = data_directory {
let data_manager = WebsiteDataManager::builder()
// TODO: Consider taking a cache_directory so this can be in XDG_CACHE_HOME.
.base_cache_directory(data_directory.to_string_lossy())
.base_data_directory(data_directory.to_string_lossy())
.build();
if let Some(cookie_manager) = data_manager.cookie_manager() {
cookie_manager.set_persistent_storage(
&data_directory.join("cookies").to_string_lossy(),
CookiePersistentStorage::Text,
);
}
context_builder = context_builder.website_data_manager(&data_manager);
}
let context = context_builder.build();
Self::create_context(context)
}
pub fn new_ephemeral() -> Self {
let context = WebContext::new_ephemeral();
Self::create_context(context)
}
pub fn create_context(context: WebContext) -> Self {
let automation = false;
context.set_automation_allowed(automation);
// e.g. wry 0.9.4
let app_info = ApplicationInfo::new();
app_info.set_name(env!("CARGO_PKG_NAME"));
app_info.set_version(
env!("CARGO_PKG_VERSION_MAJOR")
.parse()
.expect("invalid wry version major"),
env!("CARGO_PKG_VERSION_MINOR")
.parse()
.expect("invalid wry version minor"),
env!("CARGO_PKG_VERSION_PATCH")
.parse()
.expect("invalid wry version patch"),
);
Self {
context,
automation,
app_info: Some(app_info),
}
}
pub fn set_allows_automation(&mut self, flag: bool) {
self.automation = flag;
self.context.set_automation_allowed(flag);
}
pub fn set_web_extensions_directory(&mut self, path: &Path) {
self
.context
.set_web_extensions_directory(&path.to_string_lossy());
}
}
/// [`WebContext`](super::WebContext) items that only matter on unix.
pub trait WebContextExt {
/// The GTK [`WebContext`] of all webviews in the context.
fn context(&self) -> &WebContext;
/// Register a custom protocol to the web context.
fn register_uri_scheme<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(crate::WebViewId, Request<Vec<u8>>, RequestAsyncResponder) + 'static;
/// Loads a URI for a [`WebView`].
fn load_uri(&self, webview: WebView, url: String, headers: Option<http::HeaderMap>);
/// If the context allows automation.
///
/// **Note:** `libwebkit2gtk` only allows 1 automation context at a time.
fn allows_automation(&self) -> bool;
fn register_automation(&mut self, webview: WebView);
fn register_download_handler(
&mut self,
download_started_callback: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool>>,
download_completed_callback: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
);
}
impl WebContextExt for super::WebContext {
fn context(&self) -> &WebContext {
&self.os.context
}
fn register_uri_scheme<F>(&mut self, name: &str, handler: F) -> crate::Result<()>
where
F: Fn(crate::WebViewId, Request<Vec<u8>>, RequestAsyncResponder) + 'static,
{
self.register_custom_protocol(name.to_owned())?;
// Enable secure context
self
.os
.context
.security_manager()
.ok_or(Error::MissingManager)?
.register_uri_scheme_as_secure(name);
self.os.context.register_uri_scheme(name, move |request| {
#[cfg(feature = "tracing")]
let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty).entered();
if let Some(uri) = request.uri() {
let uri = uri.as_str();
#[cfg(feature = "tracing")]
span.record("uri", uri);
#[allow(unused_mut)]
let mut http_request = Request::builder().uri(uri).method("GET");
// Set request http headers
if let Some(headers) = request.http_headers() {
if let Some(map) = http_request.headers_mut() {
headers.foreach(move |k, v| {
if let Ok(name) = HeaderName::from_bytes(k.as_bytes()) {
if let Ok(value) = HeaderValue::from_bytes(v.as_bytes()) {
map.insert(name, value);
}
}
});
}
}
// Set request http method
if let Some(method) = request.http_method() {
http_request = http_request.method(method.as_str());
}
let body;
#[cfg(feature = "linux-body")]
{
use gtk::{gdk::prelude::InputStreamExtManual, gio::Cancellable};
// Set request http body
let cancellable: Option<&Cancellable> = None;
body = request
.http_body()
.map(|s| {
const BUFFER_LEN: usize = 1024;
let mut result = Vec::new();
let mut buffer = vec![0; BUFFER_LEN];
while let Ok(count) = s.read(&mut buffer[..], cancellable) {
if count == BUFFER_LEN {
result.append(&mut buffer);
buffer.resize(BUFFER_LEN, 0);
} else {
buffer.truncate(count);
result.append(&mut buffer);
break;
}
}
result
})
.unwrap_or_default();
}
#[cfg(not(feature = "linux-body"))]
{
body = Vec::new();
}
let http_request = match http_request.body(body) {
Ok(req) => req,
Err(_) => {
request.finish_error(&mut gtk::glib::Error::new(
glib::UriError::Failed,
"Internal server error: could not create request.",
));
return;
}
};
let request_ = MainThreadRequest(request.clone());
let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
Box::new(move |http_response| {
MainContext::default().invoke(move || {
let buffer = http_response.body();
let input = gtk::gio::MemoryInputStream::from_bytes(&gtk::glib::Bytes::from(buffer));
let content_type = http_response
.headers()
.get(CONTENT_TYPE)
.and_then(|h| h.to_str().ok());
let response = URISchemeResponse::new(&input, buffer.len() as i64);
response.set_status(http_response.status().as_u16() as u32, None);
if let Some(content_type) = content_type {
response.set_content_type(content_type);
}
let headers = MessageHeaders::new(MessageHeadersType::Response);
for (name, value) in http_response.headers().into_iter() {
headers.append(name.as_str(), value.to_str().unwrap_or(""));
}
response.set_http_headers(headers);
request_.finish_with_response(&response);
});
});
#[cfg(feature = "tracing")]
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
let webview_id = request
.web_view()
.and_then(|w| unsafe { w.data::<String>(super::WEBVIEW_ID) })
.map(|id| unsafe { id.as_ref().clone() })
.unwrap_or_default();
handler(&webview_id, http_request, RequestAsyncResponder { responder });
} else {
request.finish_error(&mut glib::Error::new(
glib::FileError::Exist,
"Could not get uri.",
));
}
});
Ok(())
}
fn load_uri(&self, webview: WebView, uri: String, headers: Option<http::HeaderMap>) {
if let Some(headers) = headers {
let req = URIRequest::builder().uri(&uri).build();
if let Some(ref mut req_headers) = req.http_headers() {
for (header, value) in headers.iter() {
req_headers.append(
header.to_string().as_str(),
value.to_str().unwrap_or_default(),
);
}
}
webview.load_request(&req);
} else {
webview.load_uri(&uri);
}
}
fn allows_automation(&self) -> bool {
self.os.automation
}
fn register_automation(&mut self, webview: WebView) {
if let (true, Some(app_info)) = (self.os.automation, self.os.app_info.take()) {
self.os.context.connect_automation_started(move |_, auto| {
let webview = webview.clone();
auto.set_application_info(&app_info);
// We do **NOT** support arbitrarily creating new webviews.
// To support this in the future, we would need a way to specify the
// default WindowBuilder to use to create the window it will use, and
// possibly "default" webview attributes. Difficulty comes in for controlling
// the owned Window that would need to be used.
//
// Instead, we just pass the first created webview.
auto.connect_create_web_view(None, move |_| webview.clone());
});
}
}
fn register_download_handler(
&mut self,
download_started_handler: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool>>,
download_completed_handler: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
) {
let context = &self.os.context;
let download_started_handler = Rc::new(RefCell::new(download_started_handler));
let failed = Rc::new(RefCell::new(false));
context.connect_download_started(move |_context, download| {
let download_started_handler = download_started_handler.clone();
download.connect_decide_destination(move |download, suggested_filename| {
if let Some(uri) = download.request().and_then(|req| req.uri()) {
let uri = uri.to_string();
if let Some(download_started_handler) = download_started_handler.borrow_mut().as_mut() {
let mut download_destination =
dirs::download_dir().unwrap_or_else(|| current_dir().unwrap_or_default());
let (mut suggested_filename, ext) = suggested_filename
.split_once('.')
.map(|(base, ext)| (base, format!(".{ext}")))
.unwrap_or((suggested_filename, "".to_string()));
// For `data:` downloads, webkitgtk will suggest to use the raw data as the filename if the dev provided no name,
// for example `"data:attachment/text,sometext"` will result in `text,sometext` but longer data URLs will
// result in a cut-off filename, which makes it hard to predict reliably.
// TODO: If this keeps causing problems, just remove it and use whatever file name webkitgtk suggests.
if uri.starts_with("data:") {
if let Some((_, uri_stripped)) = uri.split_once('/') {
if let Some((uri_stripped, _)) = uri_stripped.split_once(',') {
if suggested_filename.starts_with(&format!("{uri_stripped},")) {
suggested_filename = "Unknown";
}
}
}
}
download_destination.push(format!("{suggested_filename}{ext}"));
// WebView2 does not overwrite files but appends numbers
let mut counter = 1;
while download_destination.exists() {
download_destination.set_file_name(format!("{suggested_filename} ({counter}){ext}"));
counter += 1;
}
if download_started_handler(uri, &mut download_destination) {
download.set_destination(&download_destination.to_string_lossy());
} else {
download.cancel();
}
}
}
// TODO: check if we may also need `false`
true
});
download.connect_failed({
let failed = failed.clone();
move |_, _error| {
*failed.borrow_mut() = true;
}
});
if let Some(download_completed_handler) = download_completed_handler.clone() {
download.connect_finished({
let failed = failed.clone();
move |download| {
if let Some(uri) = download.request().and_then(|req| req.uri()) {
let failed = *failed.borrow();
let uri = uri.to_string();
download_completed_handler(
uri,
(!failed)
.then(|| download.destination().map(PathBuf::from))
.flatten(),
!failed,
)
}
}
});
}
});
}
}
struct MainThreadRequest(URISchemeRequest);
impl MainThreadRequest {
fn finish_with_response(&self, response: &URISchemeResponse) {
self.0.finish_with_response(response);
}
}
unsafe impl Send for MainThreadRequest {}
unsafe impl Sync for MainThreadRequest {}

251
vendor/wry/src/webview2/drag_drop.rs vendored Normal file
View File

@ -0,0 +1,251 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// A silly implementation of file drop handling for Windows!
use crate::DragDropEvent;
use std::{
cell::UnsafeCell,
ffi::OsString,
os::{raw::c_void, windows::ffi::OsStringExt},
path::PathBuf,
ptr,
rc::Rc,
};
use windows::{
core::{implement, BOOL},
Win32::{
Foundation::{DRAGDROP_E_INVALIDHWND, HWND, LPARAM, POINT, POINTL},
Graphics::Gdi::ScreenToClient,
System::{
Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL},
Ole::{
IDropTarget, IDropTarget_Impl, RegisterDragDrop, RevokeDragDrop, CF_HDROP, DROPEFFECT,
DROPEFFECT_COPY, DROPEFFECT_NONE,
},
SystemServices::MODIFIERKEYS_FLAGS,
},
UI::{
Shell::{DragFinish, DragQueryFileW, HDROP},
WindowsAndMessaging::EnumChildWindows,
},
},
};
#[derive(Default)]
pub(crate) struct DragDropController {
drop_targets: Vec<IDropTarget>,
}
impl DragDropController {
#[inline]
pub(crate) fn new(hwnd: HWND, handler: Box<dyn Fn(DragDropEvent) -> bool>) -> Self {
let mut controller = DragDropController::default();
let handler = Rc::new(handler);
// Enumerate child windows to find the WebView2 "window" and override!
{
let mut callback = |hwnd| controller.inject_in_hwnd(hwnd, handler.clone());
let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback;
let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) };
let lparam = LPARAM(closure_pointer_pointer as _);
unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
let closure = &mut *(lparam.0 as *mut c_void as *mut &mut dyn FnMut(HWND) -> bool);
closure(hwnd).into()
}
let _ = unsafe { EnumChildWindows(Some(hwnd), Some(enumerate_callback), lparam) };
}
controller
}
#[inline]
fn inject_in_hwnd(&mut self, hwnd: HWND, handler: Rc<dyn Fn(DragDropEvent) -> bool>) -> bool {
let drag_drop_target: IDropTarget = DragDropTarget::new(hwnd, handler).into();
if unsafe { RevokeDragDrop(hwnd) } != Err(DRAGDROP_E_INVALIDHWND.into())
&& unsafe { RegisterDragDrop(hwnd, &drag_drop_target) }.is_ok()
{
self.drop_targets.push(drag_drop_target);
}
true
}
}
#[implement(IDropTarget)]
pub struct DragDropTarget {
hwnd: HWND,
listener: Rc<dyn Fn(DragDropEvent) -> bool>,
cursor_effect: UnsafeCell<DROPEFFECT>,
enter_is_valid: UnsafeCell<bool>, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */
}
impl DragDropTarget {
pub fn new(hwnd: HWND, listener: Rc<dyn Fn(DragDropEvent) -> bool>) -> DragDropTarget {
Self {
hwnd,
listener,
cursor_effect: DROPEFFECT_NONE.into(),
enter_is_valid: false.into(),
}
}
unsafe fn iterate_filenames<F>(
data_obj: windows_core::Ref<'_, IDataObject>,
mut callback: F,
) -> Option<HDROP>
where
F: FnMut(PathBuf),
{
let drop_format = FORMATETC {
cfFormat: CF_HDROP.0,
ptd: ptr::null_mut(),
dwAspect: DVASPECT_CONTENT.0,
lindex: -1,
tymed: TYMED_HGLOBAL.0 as u32,
};
match data_obj
.as_ref()
.expect("Received null IDataObject")
.GetData(&drop_format)
{
Ok(medium) => {
let hdrop = HDROP(medium.u.hGlobal.0 as _);
// The second parameter (0xFFFFFFFF) instructs the function to return the item count
let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, None);
for i in 0..item_count {
// Get the length of the path string NOT including the terminating null character.
// Previously, this was using a fixed size array of MAX_PATH length, but the
// Windows API allows longer paths under certain circumstances.
let character_count = DragQueryFileW(hdrop, i, None) as usize;
// Fill path_buf with the null-terminated file name
let str_len = character_count + 1;
let mut path_buf = vec![0; str_len];
DragQueryFileW(hdrop, i, Some(&mut path_buf));
callback(OsString::from_wide(&path_buf[0..character_count]).into());
}
Some(hdrop)
}
Err(_error) => {
#[cfg(feature = "tracing")]
tracing::warn!(
"{}",
match _error.code() {
windows::Win32::Foundation::DV_E_FORMATETC => {
// If the dropped item is not a file this error will occur.
// In this case it is OK to return without taking further action.
"Error occurred while processing dropped/hovered item: item is not a file."
}
_ => "Unexpected error occurred while processing dropped/hovered item.",
}
);
None
}
}
}
}
#[allow(non_snake_case)]
impl IDropTarget_Impl for DragDropTarget_Impl {
fn DragEnter(
&self,
pDataObj: windows_core::Ref<'_, IDataObject>,
_grfKeyState: MODIFIERKEYS_FLAGS,
pt: &POINTL,
pdwEffect: *mut DROPEFFECT,
) -> windows::core::Result<()> {
let mut pt = POINT { x: pt.x, y: pt.y };
let _ = unsafe { ScreenToClient(self.hwnd, &mut pt) };
let mut paths = Vec::new();
let hdrop = unsafe { DragDropTarget::iterate_filenames(pDataObj, |path| paths.push(path)) };
let enter_is_valid = hdrop.is_some();
if !enter_is_valid {
return Ok(());
};
unsafe {
*self.enter_is_valid.get() = enter_is_valid;
}
(self.listener)(DragDropEvent::Enter {
paths,
position: (pt.x as _, pt.y as _),
});
let cursor_effect = if enter_is_valid {
DROPEFFECT_COPY
} else {
DROPEFFECT_NONE
};
unsafe {
*pdwEffect = cursor_effect;
*self.cursor_effect.get() = cursor_effect;
}
Ok(())
}
fn DragOver(
&self,
_grfKeyState: MODIFIERKEYS_FLAGS,
pt: &POINTL,
pdwEffect: *mut DROPEFFECT,
) -> windows::core::Result<()> {
if unsafe { *self.enter_is_valid.get() } {
let mut pt = POINT { x: pt.x, y: pt.y };
let _ = unsafe { ScreenToClient(self.hwnd, &mut pt) };
(self.listener)(DragDropEvent::Over {
position: (pt.x as _, pt.y as _),
});
}
unsafe { *pdwEffect = *self.cursor_effect.get() };
Ok(())
}
fn DragLeave(&self) -> windows::core::Result<()> {
if unsafe { *self.enter_is_valid.get() } {
(self.listener)(DragDropEvent::Leave);
}
Ok(())
}
fn Drop(
&self,
pDataObj: windows_core::Ref<'_, IDataObject>,
_grfKeyState: MODIFIERKEYS_FLAGS,
pt: &POINTL,
_pdwEffect: *mut DROPEFFECT,
) -> windows::core::Result<()> {
if unsafe { *self.enter_is_valid.get() } {
let mut pt = POINT { x: pt.x, y: pt.y };
let _ = unsafe { ScreenToClient(self.hwnd, &mut pt) };
let mut paths = Vec::new();
let hdrop = unsafe { DragDropTarget::iterate_filenames(pDataObj, |path| paths.push(path)) };
(self.listener)(DragDropEvent::Drop {
paths,
position: (pt.x as _, pt.y as _),
});
if let Some(hdrop) = hdrop {
unsafe { DragFinish(hdrop) };
}
}
Ok(())
}
}

1909
vendor/wry/src/webview2/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

101
vendor/wry/src/webview2/util.rs vendored Normal file
View File

@ -0,0 +1,101 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use once_cell::sync::Lazy;
use windows::{
core::{HRESULT, HSTRING, PCSTR},
Win32::{
Foundation::{FARPROC, HWND, S_OK},
Graphics::Gdi::{
GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST,
},
System::LibraryLoader::{GetProcAddress, LoadLibraryW},
UI::{
HiDpi::{MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE},
WindowsAndMessaging::IsProcessDPIAware,
},
},
};
fn get_function_impl(library: &str, function: &str) -> FARPROC {
let library = HSTRING::from(library);
assert_eq!(function.chars().last(), Some('\0'));
// Library names we will use are ASCII so we can use the A version to avoid string conversion.
let module = unsafe { LoadLibraryW(&library) }.unwrap_or_default();
if module.is_invalid() {
return None;
}
unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) }
}
macro_rules! get_function {
($lib:expr, $func:ident) => {
crate::webview2::util::get_function_impl($lib, concat!(stringify!($func), '\0'))
.map(|f| unsafe { std::mem::transmute::<_, $func>(f) })
};
}
pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32;
pub type GetDpiForMonitor = unsafe extern "system" fn(
hmonitor: HMONITOR,
dpi_type: MONITOR_DPI_TYPE,
dpi_x: *mut u32,
dpi_y: *mut u32,
) -> HRESULT;
static GET_DPI_FOR_WINDOW: Lazy<Option<GetDpiForWindow>> =
Lazy::new(|| get_function!("user32.dll", GetDpiForWindow));
static GET_DPI_FOR_MONITOR: Lazy<Option<GetDpiForMonitor>> =
Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor));
pub const BASE_DPI: u32 = 96;
pub fn dpi_to_scale_factor(dpi: u32) -> f64 {
dpi as f64 / BASE_DPI as f64
}
#[allow(non_snake_case)]
pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 {
if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW {
// We are on Windows 10 Anniversary Update (1607) or later.
match GetDpiForWindow(hwnd) {
0 => BASE_DPI, // 0 is returned if hwnd is invalid
#[allow(clippy::unnecessary_cast)]
dpi => dpi as u32,
}
} else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR {
// We are on Windows 8.1 or later.
let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if monitor.is_invalid() {
return BASE_DPI;
}
let mut dpi_x = 0;
let mut dpi_y = 0;
#[allow(clippy::unnecessary_cast)]
if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK {
dpi_x as u32
} else {
BASE_DPI
}
} else {
let hdc = GetDC(Some(hwnd));
if hdc.is_invalid() {
return BASE_DPI;
}
// We are on Vista or later.
if IsProcessDPIAware().as_bool() {
// If the process is DPI aware, then scaling must be handled by the application using
// this DPI value.
GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32
} else {
// If the process is DPI unaware, then scaling is performed by the OS; we thus return
// 96 (scale factor 1.0) to prevent the window from being re-scaled by both the
// application and the WM.
BASE_DPI
}
}
}

View File

@ -0,0 +1,90 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{ffi::c_void, ptr::null_mut};
use objc2::{
define_class, msg_send,
rc::Retained,
runtime::{AnyObject, NSObject},
AllocAnyThread, DefinedClass,
};
use objc2_foundation::{
ns_string, NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions,
NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSString,
};
use crate::WryWebView;
pub struct DocumentTitleChangedObserverIvars {
pub object: Retained<WryWebView>,
pub handler: Box<dyn Fn(String)>,
}
define_class!(
#[unsafe(super(NSObject))]
#[ivars = DocumentTitleChangedObserverIvars]
pub struct DocumentTitleChangedObserver;
/// NSKeyValueObserving.
impl DocumentTitleChangedObserver {
#[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
fn observe_value_for_key_path(
&self,
key_path: Option<&NSString>,
of_object: Option<&AnyObject>,
_change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
_context: *mut c_void,
) {
if let (Some(key_path), Some(object)) = (key_path, of_object) {
unsafe {
if key_path.isEqualToString(ns_string!("title")) {
let handler = &self.ivars().handler;
// if !handler.is_null() {
let title: *const NSString = msg_send![object, title];
handler((*title).to_string());
// }
}
}
}
}
}
unsafe impl NSObjectProtocol for DocumentTitleChangedObserver {}
);
impl DocumentTitleChangedObserver {
pub fn new(webview: Retained<WryWebView>, handler: Box<dyn Fn(String)>) -> Retained<Self> {
let observer = Self::alloc().set_ivars(DocumentTitleChangedObserverIvars {
object: webview,
handler,
});
let observer: Retained<Self> = unsafe { msg_send![super(observer), init] };
unsafe {
observer
.ivars()
.object
.addObserver_forKeyPath_options_context(
&observer,
ns_string!("title"),
NSKeyValueObservingOptions::New,
null_mut(),
);
}
observer
}
}
impl Drop for DocumentTitleChangedObserver {
fn drop(&mut self) {
unsafe {
self
.ivars()
.object
.removeObserver_forKeyPath(self, ns_string!("title"));
}
}
}

12
vendor/wry/src/wkwebview/class/mod.rs vendored Normal file
View File

@ -0,0 +1,12 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
pub mod document_title_changed_observer;
pub mod url_scheme_handler;
pub mod wry_download_delegate;
pub mod wry_navigation_delegate;
pub mod wry_web_view;
pub mod wry_web_view_delegate;
pub mod wry_web_view_parent;
pub mod wry_web_view_ui_delegate;

View File

@ -0,0 +1,346 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
borrow::Cow,
ffi::{c_char, c_void, CStr},
panic::AssertUnwindSafe,
ptr::NonNull,
};
use http::{
header::{CONTENT_LENGTH, CONTENT_TYPE},
Request, Response as HttpResponse, StatusCode, Version,
};
use objc2::{
rc::Retained,
runtime::{AnyClass, AnyObject, ClassBuilder, ProtocolObject},
AllocAnyThread, ClassType, Message,
};
use objc2_foundation::{
NSData, NSHTTPURLResponse, NSMutableDictionary, NSObject, NSObjectProtocol, NSString, NSURL,
NSUUID,
};
use objc2_web_kit::{WKURLSchemeHandler, WKURLSchemeTask};
use crate::{wkwebview::WEBVIEW_STATE, RequestAsyncResponder, WryWebView};
pub fn create(name: &str) -> &AnyClass {
unsafe {
// Include the address of WEBVIEW_STATE in the class name so that each dylib in the process
// gets its own ObjC class with method pointers into its own code and data segments.
let unique_id = std::ptr::addr_of!(WEBVIEW_STATE) as usize;
let scheme_name = format!("{name}URLSchemeHandler_{unique_id:x}\0");
let scheme_name = CStr::from_bytes_with_nul(scheme_name.as_bytes()).unwrap();
let cls = ClassBuilder::new(scheme_name, NSObject::class());
match cls {
Some(mut cls) => {
cls.add_ivar::<*mut c_char>(c"webview_id");
cls.add_ivar::<usize>(c"protocol_index");
cls.add_method(
objc2::sel!(webView:startURLSchemeTask:),
start_task as extern "C" fn(_, _, _, _),
);
cls.add_method(
objc2::sel!(webView:stopURLSchemeTask:),
stop_task as extern "C" fn(_, _, _, _),
);
cls.register()
}
None => AnyClass::get(scheme_name).expect("Failed to get the class definition"),
}
}
}
// Task handler for custom protocol
extern "C" fn start_task(
this: &AnyObject,
_sel: objc2::runtime::Sel,
webview: &WryWebView,
task: &ProtocolObject<dyn WKURLSchemeTask>,
) {
unsafe {
#[cfg(feature = "tracing")]
let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty)
.entered();
let task_key = task.hash(); // hash by task object address
let task_uuid = webview.add_custom_task_key(task_key);
let ivar = this.class().instance_variable(c"webview_id").unwrap();
let webview_id_ptr: *mut c_char = *ivar.load(this);
let webview_id = CStr::from_ptr(webview_id_ptr)
.to_str()
.ok()
.unwrap_or_default();
let ivar = this.class().instance_variable(c"protocol_index").unwrap();
let protocol_index: usize = *ivar.load(this);
let function = WEBVIEW_STATE
.read()
.unwrap()
.get(webview_id)
.and_then(|v| v.protocol_ptrs.get(protocol_index))
.cloned();
if let Some(function) = function {
// Get url request
let request = task.request();
let url = request.URL().unwrap();
let uri = url.absoluteString().unwrap().to_string();
#[cfg(feature = "tracing")]
span.record("uri", uri.clone());
// Get request method (GET, POST, PUT etc...)
let method = request.HTTPMethod().unwrap().to_string();
// Prepare our HttpRequest
let mut http_request = Request::builder().uri(uri).method(method.as_str());
// Get body
let mut sent_form_body = Vec::new();
let body = request.HTTPBody();
let body_stream = request.HTTPBodyStream();
if let Some(body) = body {
sent_form_body = body.to_vec();
} else if let Some(body_stream) = body_stream {
body_stream.open();
while body_stream.hasBytesAvailable() {
sent_form_body.reserve(128);
let p = sent_form_body.as_mut_ptr().add(sent_form_body.len());
let read_length = sent_form_body.capacity() - sent_form_body.len();
let count = body_stream.read_maxLength(NonNull::new(p).unwrap(), read_length);
sent_form_body.set_len(sent_form_body.len() + count as usize);
}
body_stream.close();
}
// Extract all headers fields
let all_headers = request.allHTTPHeaderFields();
// get all our headers values and inject them in our request
if let Some(all_headers) = all_headers {
for current_header in all_headers.allKeys().iter() {
let header_value = all_headers.valueForKey(&current_header).unwrap();
// inject the header into the request
http_request = http_request.header(current_header.to_string(), header_value.to_string());
}
}
let respond_with_404 = || {
let urlresponse = NSHTTPURLResponse::alloc();
let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields(
urlresponse,
&url,
StatusCode::NOT_FOUND.as_u16().try_into().unwrap(),
Some(&NSString::from_str(
format!("{:#?}", Version::HTTP_11).as_str(),
)),
None,
)
.unwrap();
task.didReceiveResponse(&response);
// Finish
task.didFinish();
};
fn check_webview_id_valid(webview_id: &str) -> crate::Result<()> {
if !WEBVIEW_STATE.read().unwrap().contains_key(webview_id) {
return Err(crate::Error::CustomProtocolTaskInvalid);
}
Ok(())
}
/// Task may not live longer than async custom protocol handler.
///
/// There are roughly 2 ways to cause segfault:
/// 1. Task has stopped. pointer of the task not valid anymore.
/// 2. Task had stopped, but the pointer of the task has allocated to a new task.
/// Outdated custom handler may call to the new task instance and cause segfault.
fn check_task_is_valid(
webview: &WryWebView,
task_key: usize,
current_uuid: Retained<NSUUID>,
) -> crate::Result<()> {
let latest_task_uuid = webview.get_custom_task_uuid(task_key);
let Some(latest_uuid) = latest_task_uuid else {
return Err(crate::Error::CustomProtocolTaskInvalid);
};
if latest_uuid != current_uuid {
return Err(crate::Error::CustomProtocolTaskInvalid);
}
Ok(())
}
// send response
match http_request.body(sent_form_body) {
Ok(final_request) => {
let webview = webview.retain();
let task = task.retain();
let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
Box::new(move |sent_response| {
// Consolidate checks before calling into `did*` methods.
let validate = || -> crate::Result<()> {
check_webview_id_valid(webview_id)?;
check_task_is_valid(&webview, task_key, task_uuid.clone())?;
Ok(())
};
// Perform an upfront validation
if let Err(_e) = validate() {
#[cfg(feature = "tracing")]
tracing::warn!("Task invalid before sending response: {:?}", _e);
return; // If invalid, return early without calling task methods.
}
unsafe fn response(
// FIXME: though we give it a static lifetime, it's not guaranteed to be valid.
task: Retained<ProtocolObject<dyn WKURLSchemeTask>>,
// FIXME: though we give it a static lifetime, it's not guaranteed to be valid.
webview: Retained<WryWebView>,
task_key: usize,
task_uuid: Retained<NSUUID>,
webview_id: &str,
url: Retained<NSURL>,
sent_response: HttpResponse<Cow<'_, [u8]>>,
) -> crate::Result<()> {
// Validate
check_webview_id_valid(webview_id)?;
check_task_is_valid(&webview, task_key, task_uuid.clone())?;
let content = sent_response.body();
// default: application/octet-stream, but should be provided by the client
let wanted_mime = sent_response.headers().get(CONTENT_TYPE);
// default to 200
let wanted_status_code = sent_response.status().as_u16() as i32;
// default to HTTP/1.1
let wanted_version = format!("{:#?}", sent_response.version());
let headers = NSMutableDictionary::new();
if let Some(mime) = wanted_mime {
headers.insert(
&*NSString::from_str(CONTENT_TYPE.as_str()),
&*NSString::from_str(mime.to_str().unwrap()),
);
}
headers.insert(
&*NSString::from_str(CONTENT_LENGTH.as_str()),
&*NSString::from_str(&content.len().to_string()),
);
// add headers
for (name, value) in sent_response.headers().iter() {
if let Ok(value) = value.to_str() {
headers.insert(
&*NSString::from_str(name.as_str()),
&*NSString::from_str(value),
);
}
}
let urlresponse = NSHTTPURLResponse::alloc();
let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields(
urlresponse,
&url,
wanted_status_code.try_into().unwrap(),
Some(&NSString::from_str(&wanted_version)),
Some(&headers),
)
.unwrap();
// Re-validate before calling didReceiveResponse
check_webview_id_valid(webview_id)?;
check_task_is_valid(&webview, task_key, task_uuid.clone())?;
// Use map_err to convert Option<Retained<Exception>> to crate::Error
objc2::exception::catch(AssertUnwindSafe(|| {
task.didReceiveResponse(&response);
}))
.map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?;
// Send data
let data = NSData::alloc();
// MIGRATE NOTE: we copied the content to the NSData because content will be freed
// when out of scope but NSData will also free the content when it's done and cause doube free.
let data = NSData::initWithBytes_length(
data,
content.as_ptr() as *mut c_void,
content.len(),
);
// Check validity again
check_webview_id_valid(webview_id)?;
check_task_is_valid(&webview, task_key, task_uuid.clone())?;
objc2::exception::catch(AssertUnwindSafe(|| {
task.didReceiveData(&data);
}))
.map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?;
check_webview_id_valid(webview_id)?;
check_task_is_valid(&webview, task_key, task_uuid)?;
objc2::exception::catch(AssertUnwindSafe(|| {
task.didFinish();
}))
.map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?;
if WEBVIEW_STATE.read().unwrap().contains_key(webview_id) {
webview.remove_custom_task_key(task_key);
Ok(())
} else {
Err(crate::Error::CustomProtocolTaskInvalid)
}
}
#[cfg(feature = "tracing")]
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
if let Err(_e) = response(
task,
webview,
task_key,
task_uuid,
webview_id,
url,
sent_response,
) {
#[cfg(feature = "tracing")]
tracing::error!("Error responding to task: {:?}", _e);
}
});
#[cfg(feature = "tracing")]
let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
function(
webview_id,
final_request,
RequestAsyncResponder { responder },
);
}
Err(_) => respond_with_404(),
};
} else {
#[cfg(feature = "tracing")]
tracing::warn!(
"Either WebView or WebContext instance is dropped! This handler shouldn't be called."
);
};
}
}
extern "C" fn stop_task(
_this: &ProtocolObject<dyn WKURLSchemeHandler>,
_sel: objc2::runtime::Sel,
webview: &WryWebView,
task: &ProtocolObject<dyn WKURLSchemeTask>,
) {
webview.remove_custom_task_key(task.hash());
}

View File

@ -0,0 +1,67 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{cell::RefCell, path::PathBuf, rc::Rc};
use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly};
use objc2_foundation::{
MainThreadMarker, NSData, NSError, NSObjectProtocol, NSString, NSURLResponse, NSURL,
};
use objc2_web_kit::{WKDownload, WKDownloadDelegate};
use crate::wkwebview::download::{download_did_fail, download_did_finish, download_policy};
pub struct WryDownloadDelegateIvars {
pub started: Option<RefCell<Box<dyn FnMut(String, &mut PathBuf) -> bool + 'static>>>,
pub completed: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[ivars = WryDownloadDelegateIvars]
pub struct WryDownloadDelegate;
unsafe impl NSObjectProtocol for WryDownloadDelegate {}
unsafe impl WKDownloadDelegate for WryDownloadDelegate {
#[unsafe(method(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:))]
fn download_policy(
&self,
download: &WKDownload,
response: &NSURLResponse,
suggested_filename: &NSString,
handler: &block2::Block<dyn Fn(*const NSURL)>,
) {
download_policy(self, download, response, suggested_filename, handler);
}
#[unsafe(method(downloadDidFinish:))]
fn download_did_finish(&self, download: &WKDownload) {
download_did_finish(self, download);
}
#[unsafe(method(download:didFailWithError:resumeData:))]
fn download_did_fail(&self, download: &WKDownload, error: &NSError, resume_data: &NSData) {
download_did_fail(self, download, error, resume_data);
}
}
);
impl WryDownloadDelegate {
pub fn new(
download_started_handler: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool + 'static>>,
download_completed_handler: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
mtm: MainThreadMarker,
) -> Retained<Self> {
let delegate = mtm
.alloc::<WryDownloadDelegate>()
.set_ivars(WryDownloadDelegateIvars {
started: download_started_handler.map(|handler| RefCell::new(handler)),
completed: download_completed_handler,
});
unsafe { msg_send![super(delegate), init] }
}
}

View File

@ -0,0 +1,158 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::{Arc, Mutex};
use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly};
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
use objc2_web_kit::{
WKDownload, WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationDelegate,
WKNavigationResponse, WKNavigationResponsePolicy,
};
#[cfg(target_os = "ios")]
use crate::wkwebview::ios::WKWebView::WKWebView;
#[cfg(target_os = "macos")]
use objc2_web_kit::WKWebView;
use crate::{
url_from_webview,
wkwebview::{
download::{navigation_download_action, navigation_download_response},
navigation::{
did_commit_navigation, did_finish_navigation, navigation_policy, navigation_policy_response,
web_content_process_did_terminate,
},
},
PageLoadEvent, WryWebView,
};
use super::wry_download_delegate::WryDownloadDelegate;
pub struct WryNavigationDelegateIvars {
pub pending_scripts: Arc<Mutex<Option<Vec<String>>>>,
pub has_download_handler: bool,
pub navigation_policy_function: Box<dyn Fn(String) -> bool>,
pub download_delegate: Option<Retained<WryDownloadDelegate>>,
pub on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent)>>,
pub on_web_content_process_terminate_handler: Option<Box<dyn Fn()>>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[ivars = WryNavigationDelegateIvars]
pub struct WryNavigationDelegate;
unsafe impl NSObjectProtocol for WryNavigationDelegate {}
unsafe impl WKNavigationDelegate for WryNavigationDelegate {
#[unsafe(method(webView:decidePolicyForNavigationAction:decisionHandler:))]
fn navigation_policy(
&self,
webview: &WKWebView,
action: &WKNavigationAction,
handler: &block2::Block<dyn Fn(WKNavigationActionPolicy)>,
) {
navigation_policy(self, webview, action, handler);
}
#[unsafe(method(webView:decidePolicyForNavigationResponse:decisionHandler:))]
fn navigation_policy_response(
&self,
webview: &WKWebView,
response: &WKNavigationResponse,
handler: &block2::Block<dyn Fn(WKNavigationResponsePolicy)>,
) {
navigation_policy_response(self, webview, response, handler);
}
#[unsafe(method(webView:didFinishNavigation:))]
fn did_finish_navigation(&self, webview: &WKWebView, navigation: &WKNavigation) {
did_finish_navigation(self, webview, navigation);
}
#[unsafe(method(webView:didCommitNavigation:))]
fn did_commit_navigation(&self, webview: &WKWebView, navigation: &WKNavigation) {
did_commit_navigation(self, webview, navigation);
}
#[unsafe(method(webView:navigationAction:didBecomeDownload:))]
fn navigation_download_action(
&self,
webview: &WKWebView,
action: &WKNavigationAction,
download: &WKDownload,
) {
navigation_download_action(self, webview, action, download);
}
#[unsafe(method(webView:navigationResponse:didBecomeDownload:))]
fn navigation_download_response(
&self,
webview: &WKWebView,
response: &WKNavigationResponse,
download: &WKDownload,
) {
navigation_download_response(self, webview, response, download);
}
#[unsafe(method(webViewWebContentProcessDidTerminate:))]
fn web_content_process_did_terminate(&self, webview: &WKWebView) {
web_content_process_did_terminate(self, webview);
}
}
);
impl WryNavigationDelegate {
#[allow(clippy::too_many_arguments)]
pub fn new(
webview: Retained<WryWebView>,
pending_scripts: Arc<Mutex<Option<Vec<String>>>>,
has_download_handler: bool,
navigation_handler: Option<Box<dyn Fn(String) -> bool>>,
download_delegate: Option<Retained<WryDownloadDelegate>>,
on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent, String)>>,
on_web_content_process_terminate_handler: Option<Box<dyn Fn()>>,
mtm: MainThreadMarker,
) -> Retained<Self> {
let navigation_policy_function = Box::new(move |url: String| -> bool {
navigation_handler
.as_ref()
.map_or(true, |navigation_handler| (navigation_handler)(url))
});
let on_page_load_handler = if let Some(handler) = on_page_load_handler {
let custom_handler = Box::new(move |event| {
handler(event, url_from_webview(&webview).unwrap_or_default());
}) as Box<dyn Fn(PageLoadEvent)>;
Some(custom_handler)
} else {
None
};
let on_web_content_process_terminate_handler =
if let Some(handler) = on_web_content_process_terminate_handler {
let custom_handler = Box::new(move || {
handler();
}) as Box<dyn Fn()>;
Some(custom_handler)
} else {
None
};
let delegate = mtm
.alloc::<WryNavigationDelegate>()
.set_ivars(WryNavigationDelegateIvars {
pending_scripts,
navigation_policy_function,
has_download_handler,
download_delegate,
on_page_load_handler,
on_web_content_process_terminate_handler,
});
unsafe { msg_send![super(delegate), init] }
}
}

View File

@ -0,0 +1,153 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{collections::HashMap, sync::Mutex};
#[cfg(target_os = "macos")]
use objc2::runtime::ProtocolObject;
use objc2::{define_class, rc::Retained, runtime::Bool, DeclaredClass};
#[cfg(target_os = "macos")]
use objc2_app_kit::{NSDraggingDestination, NSEvent};
use objc2_foundation::{NSObjectProtocol, NSUUID};
#[cfg(target_os = "ios")]
use crate::wkwebview::ios::WKWebView::WKWebView;
#[cfg(target_os = "macos")]
use crate::{
wkwebview::{drag_drop, synthetic_mouse_events},
DragDropEvent,
};
#[cfg(target_os = "ios")]
use objc2_ui_kit::UIEvent as NSEvent;
#[cfg(target_os = "macos")]
use objc2_web_kit::WKWebView;
pub struct WryWebViewIvars {
pub(crate) is_child: bool,
#[cfg(target_os = "macos")]
pub(crate) drag_drop_handler: Box<dyn Fn(DragDropEvent) -> bool>,
#[cfg(target_os = "macos")]
pub(crate) accept_first_mouse: objc2::runtime::Bool,
#[cfg(target_os = "ios")]
pub(crate) input_accessory_view_builder: Option<Box<crate::InputAccessoryViewBuilder>>,
pub(crate) custom_protocol_task_ids: Mutex<HashMap<usize, Retained<NSUUID>>>,
}
define_class!(
#[unsafe(super(WKWebView))]
#[ivars = WryWebViewIvars]
pub struct WryWebView;
/// Overridden NSView methods.
impl WryWebView {
#[unsafe(method(performKeyEquivalent:))]
fn perform_key_equivalent(&self, event: &NSEvent) -> Bool {
// This is a temporary workaround for https://github.com/tauri-apps/tauri/issues/9426
// FIXME: When the webview is a child webview, performKeyEquivalent always return YES
// and stop propagating the event to the window, hence the menu shortcut won't be
// triggered. However, overriding this method also means the cmd+key event won't be
// handled in webview, which means the key cannot be listened by JavaScript.
if self.ivars().is_child {
Bool::NO
} else {
unsafe { objc2::msg_send![super(self), performKeyEquivalent: event] }
}
}
#[cfg(target_os = "macos")]
#[unsafe(method(acceptsFirstMouse:))]
fn accept_first_mouse(&self, _event: &NSEvent) -> Bool {
self.ivars().accept_first_mouse
}
#[cfg(target_os = "ios")]
#[unsafe(method_id(inputAccessoryView))]
fn input_accessory_view(&self) -> Option<Retained<objc2_ui_kit::UIView>> {
if let Some(builder) = &self.ivars().input_accessory_view_builder {
builder(self)
} else {
unsafe { objc2::msg_send![super(self), inputAccessoryView] }
}
}
}
unsafe impl NSObjectProtocol for WryWebView {}
// Drag & Drop
#[cfg(target_os = "macos")]
unsafe impl NSDraggingDestination for WryWebView {
#[unsafe(method(draggingEntered:))]
fn dragging_entered(
&self,
drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
) -> objc2_app_kit::NSDragOperation {
drag_drop::dragging_entered(self, drag_info)
}
#[unsafe(method(draggingUpdated:))]
fn dragging_updated(
&self,
drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
) -> objc2_app_kit::NSDragOperation {
drag_drop::dragging_updated(self, drag_info)
}
#[unsafe(method(performDragOperation:))]
fn perform_drag_operation(
&self,
drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
) -> Bool {
drag_drop::perform_drag_operation(self, drag_info)
}
#[unsafe(method(draggingExited:))]
fn dragging_exited(&self, drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>) {
drag_drop::dragging_exited(self, drag_info)
}
}
// Synthetic mouse events
#[cfg(target_os = "macos")]
impl WryWebView {
#[unsafe(method(otherMouseDown:))]
fn other_mouse_down(&self, event: &NSEvent) {
synthetic_mouse_events::other_mouse_down(self, event)
}
#[unsafe(method(otherMouseUp:))]
fn other_mouse_up(&self, event: &NSEvent) {
synthetic_mouse_events::other_mouse_up(self, event)
}
}
);
// Custom Protocol Task Checker
impl WryWebView {
pub(crate) fn add_custom_task_key(&self, task_id: usize) -> Retained<NSUUID> {
let task_uuid = NSUUID::new();
self
.ivars()
.custom_protocol_task_ids
.lock()
.unwrap()
.insert(task_id, task_uuid.clone());
task_uuid
}
pub(crate) fn remove_custom_task_key(&self, task_id: usize) {
self
.ivars()
.custom_protocol_task_ids
.lock()
.unwrap()
.remove(&task_id);
}
pub(crate) fn get_custom_task_uuid(&self, task_id: usize) -> Option<Retained<NSUUID>> {
self
.ivars()
.custom_protocol_task_ids
.lock()
.unwrap()
.get(&task_id)
.cloned()
}
}

View File

@ -0,0 +1,105 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{ffi::CStr, panic::AssertUnwindSafe};
use http::Request;
use objc2::{
define_class, msg_send,
rc::Retained,
runtime::{NSObject, ProtocolObject},
DeclaredClass, MainThreadOnly,
};
use objc2_foundation::{ns_string, MainThreadMarker, NSObjectProtocol, NSString};
use objc2_web_kit::{WKScriptMessage, WKScriptMessageHandler, WKUserContentController};
pub const IPC_MESSAGE_HANDLER_NAME: &str = "ipc";
pub struct WryWebViewDelegateIvars {
pub controller: Retained<WKUserContentController>,
pub ipc_handler: Box<dyn Fn(Request<String>)>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[ivars = WryWebViewDelegateIvars]
pub struct WryWebViewDelegate;
unsafe impl NSObjectProtocol for WryWebViewDelegate {}
unsafe impl WKScriptMessageHandler for WryWebViewDelegate {
// Function for ipc handler
#[unsafe(method(userContentController:didReceiveScriptMessage:))]
fn did_receive(
this: &WryWebViewDelegate,
_controller: &WKUserContentController,
msg: &WKScriptMessage,
) {
// Safety: objc runtime calls are unsafe
unsafe {
#[cfg(feature = "tracing")]
let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered();
let ipc_handler = &this.ivars().ipc_handler;
let body = msg.body();
if let Ok(body) = body.downcast::<NSString>() {
let js_utf8 = body.UTF8String();
let frame_info = msg.frameInfo();
let request = frame_info.request();
let url = request.URL().unwrap();
let absolute_url = url.absoluteString().unwrap();
let url_utf8 = absolute_url.UTF8String();
if let (Ok(url), Ok(js)) = (
CStr::from_ptr(url_utf8).to_str(),
CStr::from_ptr(js_utf8).to_str(),
) {
if let Ok(r) = Request::builder().uri(url).body(js.to_string()) {
ipc_handler(r);
} else {
#[cfg(feature = "tracing")]
tracing::warn!("WebView received invalid IPC request: {}", js);
}
return;
}
}
#[cfg(feature = "tracing")]
tracing::warn!("WebView received invalid IPC call.");
}
}
}
);
impl WryWebViewDelegate {
pub fn new(
controller: Retained<WKUserContentController>,
ipc_handler: Box<dyn Fn(Request<String>)>,
mtm: MainThreadMarker,
) -> Retained<Self> {
let delegate = mtm
.alloc::<WryWebViewDelegate>()
.set_ivars(WryWebViewDelegateIvars {
ipc_handler,
controller,
});
let delegate: Retained<Self> = unsafe { msg_send![super(delegate), init] };
let proto_delegate = ProtocolObject::from_ref(&*delegate);
unsafe {
// this will increase the retain count of the delegate
let _res = objc2::exception::catch(AssertUnwindSafe(|| {
delegate
.ivars()
.controller
.addScriptMessageHandler_name(proto_delegate, ns_string!(IPC_MESSAGE_HANDLER_NAME));
}));
}
delegate
}
}

View File

@ -0,0 +1,110 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(target_os = "macos")]
use objc2::DefinedClass;
use objc2::{define_class, msg_send, rc::Retained, MainThreadOnly};
#[cfg(target_os = "macos")]
use objc2_app_kit::{NSApplication, NSEvent, NSView, NSWindow, NSWindowButton};
use objc2_foundation::MainThreadMarker;
#[cfg(target_os = "macos")]
use objc2_foundation::NSRect;
#[cfg(target_os = "ios")]
use objc2_ui_kit::UIView as NSView;
pub struct WryWebViewParentIvars {
#[cfg(target_os = "macos")]
traffic_light_inset: std::cell::Cell<Option<(f64, f64)>>,
}
define_class!(
#[unsafe(super(NSView))]
#[ivars = WryWebViewParentIvars]
pub struct WryWebViewParent;
/// Overridden NSView methods.
impl WryWebViewParent {
#[cfg(target_os = "macos")]
#[unsafe(method(keyDown:))]
fn key_down(&self, event: &NSEvent) {
let mtm = MainThreadMarker::new().unwrap();
let app = NSApplication::sharedApplication(mtm);
unsafe {
if let Some(menu) = app.mainMenu() {
menu.performKeyEquivalent(event);
}
}
}
#[cfg(target_os = "macos")]
#[unsafe(method(drawRect:))]
fn draw(&self, _dirty_rect: NSRect) {
if let Some((x, y)) = self.ivars().traffic_light_inset.get() {
unsafe { inset_traffic_lights(&self.window().unwrap(), x, y) };
}
}
}
);
impl WryWebViewParent {
#[allow(dead_code)]
pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
let delegate = WryWebViewParent::alloc(mtm).set_ivars(WryWebViewParentIvars {
#[cfg(target_os = "macos")]
traffic_light_inset: Default::default(),
});
unsafe { msg_send![super(delegate), init] }
}
#[cfg(target_os = "macos")]
pub fn set_traffic_light_inset(&self, ns_window: &NSWindow, position: dpi::Position) {
let scale_factor = NSWindow::backingScaleFactor(ns_window);
let position = position.to_logical(scale_factor);
self
.ivars()
.traffic_light_inset
.replace(Some((position.x, position.y)));
unsafe {
inset_traffic_lights(ns_window, position.x, position.y);
}
}
}
#[cfg(target_os = "macos")]
pub unsafe fn inset_traffic_lights(window: &NSWindow, x: f64, y: f64) {
let Some(close) = window.standardWindowButton(NSWindowButton::CloseButton) else {
#[cfg(feature = "tracing")]
tracing::warn!("skipping inset_traffic_lights, close button not found");
return;
};
let Some(miniaturize) = window.standardWindowButton(NSWindowButton::MiniaturizeButton) else {
#[cfg(feature = "tracing")]
tracing::warn!("skipping inset_traffic_lights, miniaturize button not found");
return;
};
let zoom = window.standardWindowButton(NSWindowButton::ZoomButton);
let title_bar_container_view = close.superview().unwrap().superview().unwrap();
let close_rect = NSView::frame(&close);
let title_bar_frame_height = close_rect.size.height + y;
let mut title_bar_rect = NSView::frame(&title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y = window.frame().size.height - title_bar_frame_height;
title_bar_container_view.setFrame(title_bar_rect);
let space_between = NSView::frame(&miniaturize).origin.x - close_rect.origin.x;
let mut window_buttons = vec![close, miniaturize];
if let Some(zoom) = zoom {
window_buttons.push(zoom);
}
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect = NSView::frame(&button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
}
}

View File

@ -0,0 +1,282 @@
// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(target_os = "macos")]
use std::{cell::RefCell, ptr::null_mut, rc::Rc};
use block2::Block;
#[cfg(target_os = "macos")]
use objc2::DefinedClass;
use objc2::{define_class, msg_send, rc::Retained, runtime::NSObject, MainThreadOnly};
#[cfg(target_os = "macos")]
use objc2_app_kit::{NSModalResponse, NSModalResponseOK, NSOpenPanel, NSWindowDelegate};
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
#[cfg(target_os = "macos")]
use objc2_foundation::{NSArray, NSURL};
#[cfg(target_os = "macos")]
use objc2_web_kit::WKOpenPanelParameters;
use objc2_web_kit::{
WKFrameInfo, WKMediaCaptureType, WKPermissionDecision, WKSecurityOrigin, WKUIDelegate,
};
use crate::{NewWindowFeatures, NewWindowResponse, WryWebView};
#[cfg(target_os = "macos")]
struct NewWindow {
#[allow(dead_code)]
ns_window: Retained<objc2_app_kit::NSWindow>,
#[allow(dead_code)]
webview: Retained<objc2_web_kit::WKWebView>,
#[allow(dead_code)]
delegate: Retained<WryNSWindowDelegate>,
}
// SAFETY: we are not using the new window at all, just dropping it on another thread
#[cfg(target_os = "macos")]
unsafe impl Send for NewWindow {}
#[cfg(target_os = "macos")]
impl Drop for NewWindow {
fn drop(&mut self) {
unsafe {
self.webview.removeFromSuperview();
}
}
}
#[cfg(target_os = "macos")]
struct WryNSWindowDelegateIvars {
on_close: Box<dyn Fn()>,
}
#[cfg(target_os = "macos")]
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[ivars = WryNSWindowDelegateIvars]
struct WryNSWindowDelegate;
unsafe impl NSObjectProtocol for WryNSWindowDelegate {}
unsafe impl NSWindowDelegate for WryNSWindowDelegate {
#[unsafe(method(windowWillClose:))]
unsafe fn will_close(&self, _notification: &objc2_foundation::NSNotification) {
let on_close = &self.ivars().on_close;
on_close();
}
}
);
#[cfg(target_os = "macos")]
impl WryNSWindowDelegate {
pub fn new(mtm: MainThreadMarker, on_close: Box<dyn Fn()>) -> Retained<Self> {
let delegate = mtm
.alloc::<WryNSWindowDelegate>()
.set_ivars(WryNSWindowDelegateIvars { on_close });
unsafe { msg_send![super(delegate), init] }
}
}
pub struct WryWebViewUIDelegateIvars {
#[cfg(target_os = "macos")]
new_window_req_handler: Option<Box<dyn Fn(String, NewWindowFeatures) -> NewWindowResponse>>,
#[cfg(target_os = "macos")]
new_windows: Rc<RefCell<Vec<NewWindow>>>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[ivars = WryWebViewUIDelegateIvars]
pub struct WryWebViewUIDelegate;
unsafe impl NSObjectProtocol for WryWebViewUIDelegate {}
unsafe impl WKUIDelegate for WryWebViewUIDelegate {
#[cfg(target_os = "macos")]
#[unsafe(method(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:))]
fn run_file_upload_panel(
&self,
_webview: &WryWebView,
open_panel_params: &WKOpenPanelParameters,
_frame: &WKFrameInfo,
handler: &block2::Block<dyn Fn(*const NSArray<NSURL>)>,
) {
unsafe {
if let Some(mtm) = MainThreadMarker::new() {
let open_panel = NSOpenPanel::openPanel(mtm);
open_panel.setCanChooseFiles(true);
let allow_multi = open_panel_params.allowsMultipleSelection();
open_panel.setAllowsMultipleSelection(allow_multi);
let allow_dir = open_panel_params.allowsDirectories();
open_panel.setCanChooseDirectories(allow_dir);
let ok: NSModalResponse = open_panel.runModal();
if ok == NSModalResponseOK {
let url = open_panel.URLs();
(*handler).call((Retained::as_ptr(&url),));
} else {
(*handler).call((null_mut(),));
}
}
}
}
#[unsafe(method(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:))]
fn request_media_capture_permission(
&self,
_webview: &WryWebView,
_origin: &WKSecurityOrigin,
_frame: &WKFrameInfo,
_capture_type: WKMediaCaptureType,
decision_handler: &Block<dyn Fn(WKPermissionDecision)>,
) {
//https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc
(*decision_handler).call((WKPermissionDecision::Grant,));
}
#[cfg(target_os = "macos")]
#[unsafe(method_id(webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:))]
unsafe fn create_web_view_for_navigation_action(
&self,
webview: &WryWebView,
configuration: &objc2_web_kit::WKWebViewConfiguration,
action: &objc2_web_kit::WKNavigationAction,
window_features: &objc2_web_kit::WKWindowFeatures,
) -> Option<Retained<objc2_web_kit::WKWebView>> {
if let Some(new_window_req_handler) = &self.ivars().new_window_req_handler {
let request = action.request();
let url = request.URL().unwrap().absoluteString().unwrap();
let current_window = webview.window().unwrap();
let screen = current_window.screen().unwrap();
let screen_frame = screen.frame();
match new_window_req_handler(
url.to_string(),
NewWindowFeatures {
size: if let (Some(width), Some(height)) =
(window_features.width(), window_features.height())
{
Some(dpi::LogicalSize::new(
width.doubleValue(),
height.doubleValue(),
))
} else {
None
},
position: if let (Some(x), Some(y)) = (window_features.x(), window_features.y()) {
Some(dpi::LogicalPosition::new(x.doubleValue(), y.doubleValue()))
} else {
None
},
opener: crate::NewWindowOpener {
webview: webview.into(),
target_configuration: configuration.into(),
},
},
) {
NewWindowResponse::Allow => {
let mtm = MainThreadMarker::new().unwrap();
let defaults = current_window.frame();
let size = objc2_foundation::NSSize::new(
window_features
.width()
.map_or(defaults.size.width, |width| width.doubleValue()),
window_features
.height()
.map_or(defaults.size.height, |height| height.doubleValue()),
);
let position = objc2_foundation::NSPoint::new(
window_features
.x()
.map_or(defaults.origin.x, |x| x.doubleValue()),
window_features.y().map_or(defaults.origin.y, |y| {
screen_frame.size.height - y.doubleValue() - size.height
}),
);
let rect = objc2_foundation::NSRect::new(position, size);
let mut flags = objc2_app_kit::NSWindowStyleMask::Titled
| objc2_app_kit::NSWindowStyleMask::Closable
| objc2_app_kit::NSWindowStyleMask::Miniaturizable;
let resizable = window_features
.allowsResizing()
.map_or(true, |resizable| resizable.boolValue());
if resizable {
flags |= objc2_app_kit::NSWindowStyleMask::Resizable;
}
let window = objc2_app_kit::NSWindow::initWithContentRect_styleMask_backing_defer(
mtm.alloc::<objc2_app_kit::NSWindow>(),
rect,
flags,
objc2_app_kit::NSBackingStoreType::Buffered,
false,
);
// SAFETY: Disable auto-release when closing windows.
// This is required when creating `NSWindow` outside a window
// controller.
window.setReleasedWhenClosed(false);
let webview = objc2_web_kit::WKWebView::initWithFrame_configuration(
mtm.alloc::<objc2_web_kit::WKWebView>(),
window.frame(),
configuration,
);
let new_windows = self.ivars().new_windows.clone();
let window_id = Retained::as_ptr(&window) as usize;
let delegate = WryNSWindowDelegate::new(
mtm,
Box::new(move || {
new_windows
.borrow_mut()
.retain(|window| Retained::as_ptr(&window.ns_window) as usize != window_id);
}),
);
window.setDelegate(Some(objc2::runtime::ProtocolObject::from_ref(&*delegate)));
window.setContentView(Some(&webview));
window.makeKeyAndOrderFront(None);
self.ivars().new_windows.borrow_mut().push(NewWindow {
ns_window: window,
webview: webview.clone(),
delegate,
});
Some(webview)
}
NewWindowResponse::Create { webview } => Some(webview),
NewWindowResponse::Deny => None,
}
} else {
None
}
}
}
);
impl WryWebViewUIDelegate {
pub fn new(
mtm: MainThreadMarker,
new_window_req_handler: Option<Box<dyn Fn(String, NewWindowFeatures) -> NewWindowResponse>>,
) -> Retained<Self> {
#[cfg(target_os = "ios")]
let _new_window_req_handler = new_window_req_handler;
let delegate = mtm
.alloc::<WryWebViewUIDelegate>()
.set_ivars(WryWebViewUIDelegateIvars {
#[cfg(target_os = "macos")]
new_window_req_handler,
#[cfg(target_os = "macos")]
new_windows: Rc::new(RefCell::new(vec![])),
});
unsafe { msg_send![super(delegate), init] }
}
}

122
vendor/wry/src/wkwebview/download.rs vendored Normal file
View File

@ -0,0 +1,122 @@
use std::{env::current_dir, ptr::null_mut};
use objc2::{rc::Retained, runtime::ProtocolObject, DeclaredClass};
use objc2_foundation::{NSData, NSError, NSString, NSURLResponse, NSURL};
use objc2_web_kit::{WKDownload, WKNavigationAction, WKNavigationResponse};
#[cfg(target_os = "ios")]
use crate::wkwebview::ios::WKWebView::WKWebView;
#[cfg(target_os = "macos")]
use objc2_web_kit::WKWebView;
use super::class::{
wry_download_delegate::WryDownloadDelegate, wry_navigation_delegate::WryNavigationDelegate,
};
// Download action handler
pub(crate) fn navigation_download_action(
this: &WryNavigationDelegate,
_webview: &WKWebView,
_action: &WKNavigationAction,
download: &WKDownload,
) {
unsafe {
if let Some(delegate) = &this.ivars().download_delegate {
let proto_delegate = ProtocolObject::from_ref(&**delegate);
download.setDelegate(Some(proto_delegate));
}
}
}
// Download response handler
pub(crate) fn navigation_download_response(
this: &WryNavigationDelegate,
_webview: &WKWebView,
_response: &WKNavigationResponse,
download: &WKDownload,
) {
unsafe {
if let Some(delegate) = &this.ivars().download_delegate {
let proto_delegate = ProtocolObject::from_ref(&**delegate);
download.setDelegate(Some(proto_delegate));
}
}
}
pub(crate) fn download_policy(
this: &WryDownloadDelegate,
download: &WKDownload,
_response: &NSURLResponse,
suggested_filename: &NSString,
completion_handler: &block2::Block<dyn Fn(*const NSURL)>,
) {
unsafe {
let request = download.originalRequest().unwrap();
let url = request.URL().unwrap().absoluteString().unwrap();
let suggested_filename = suggested_filename.to_string();
let mut download_destination =
dirs::download_dir().unwrap_or_else(|| current_dir().unwrap_or_default());
download_destination.push(&suggested_filename);
let (suggested_filename, ext) = suggested_filename
.split_once('.')
.map(|(base, ext)| (base, format!(".{ext}")))
.unwrap_or((&suggested_filename, "".to_string()));
// WebView2 does not overwrite files but appends numbers
let mut counter = 1;
while download_destination.exists() {
download_destination.set_file_name(format!("{suggested_filename} ({counter}){ext}"));
counter += 1;
}
let started_fn = &this.ivars().started;
if let Some(started_fn) = started_fn {
let mut started_fn = started_fn.borrow_mut();
match started_fn(url.to_string(), &mut download_destination) {
true => {
let path = NSString::from_str(&download_destination.display().to_string());
let ns_url = NSURL::fileURLWithPath_isDirectory(&path, false);
(*completion_handler).call((Retained::as_ptr(&ns_url),))
}
false => (*completion_handler).call((null_mut(),)),
};
} else {
#[cfg(feature = "tracing")]
tracing::warn!("WebView instance is dropped! This navigation handler shouldn't be called.");
(*completion_handler).call((null_mut(),));
}
}
}
pub(crate) fn download_did_finish(this: &WryDownloadDelegate, download: &WKDownload) {
unsafe {
let original_request = download.originalRequest().unwrap();
let url = original_request.URL().unwrap().absoluteString().unwrap();
if let Some(completed_fn) = this.ivars().completed.clone() {
completed_fn(url.to_string(), None, true);
}
}
}
pub(crate) fn download_did_fail(
this: &WryDownloadDelegate,
download: &WKDownload,
error: &NSError,
_resume_data: &NSData,
) {
unsafe {
#[cfg(debug_assertions)]
{
let description = error.localizedDescription().to_string();
eprintln!("Download failed with error: {description}");
}
let original_request = download.originalRequest().unwrap();
let url = original_request.URL().unwrap().absoluteString().unwrap();
if let Some(completed_fn) = this.ivars().completed.clone() {
completed_fn(url.to_string(), None, false);
}
}
}

104
vendor/wry/src/wkwebview/drag_drop.rs vendored Normal file
View File

@ -0,0 +1,104 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{ffi::CStr, path::PathBuf};
use objc2::{
runtime::{Bool, ProtocolObject},
DeclaredClass,
};
use objc2_app_kit::{NSDragOperation, NSDraggingInfo, NSFilenamesPboardType};
use objc2_foundation::{NSArray, NSPoint, NSRect, NSString};
use crate::DragDropEvent;
use super::WryWebView;
pub(crate) unsafe fn collect_paths(drag_info: &ProtocolObject<dyn NSDraggingInfo>) -> Vec<PathBuf> {
let pb = drag_info.draggingPasteboard();
let mut drag_drop_paths = Vec::new();
let types = NSArray::arrayWithObject(NSFilenamesPboardType);
if pb.availableTypeFromArray(&types).is_some() {
let paths = pb.propertyListForType(NSFilenamesPboardType).unwrap();
let paths = paths.downcast::<NSArray>().unwrap();
for path in paths {
let path = path.downcast::<NSString>().unwrap();
let path = CStr::from_ptr(path.UTF8String()).to_string_lossy();
drag_drop_paths.push(PathBuf::from(path.into_owned()));
}
}
drag_drop_paths
}
pub(crate) fn dragging_entered(
this: &WryWebView,
drag_info: &ProtocolObject<dyn NSDraggingInfo>,
) -> NSDragOperation {
let paths = unsafe { collect_paths(drag_info) };
let dl: NSPoint = unsafe { drag_info.draggingLocation() };
let frame: NSRect = this.frame();
let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
let listener = &this.ivars().drag_drop_handler;
if !listener(DragDropEvent::Enter { paths, position }) {
// Reject the Wry file drop (invoke the OS default behaviour)
unsafe { objc2::msg_send![super(this), draggingEntered: drag_info] }
} else {
NSDragOperation::Copy
}
}
pub(crate) fn dragging_updated(
this: &WryWebView,
drag_info: &ProtocolObject<dyn NSDraggingInfo>,
) -> NSDragOperation {
let dl: NSPoint = unsafe { drag_info.draggingLocation() };
let frame: NSRect = this.frame();
let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
let listener = &this.ivars().drag_drop_handler;
if !listener(DragDropEvent::Over { position }) {
unsafe {
let os_operation = objc2::msg_send![super(this), draggingUpdated: drag_info];
if os_operation == NSDragOperation::None {
// 0 will be returned for a drop on any arbitrary location on the webview.
// We'll override that with NSDragOperationCopy.
NSDragOperation::Copy
} else {
// A different NSDragOperation is returned when a file is hovered over something like
// a <input type="file">, so we'll make sure to preserve that behaviour.
os_operation
}
}
} else {
NSDragOperation::Copy
}
}
pub(crate) fn perform_drag_operation(
this: &WryWebView,
drag_info: &ProtocolObject<dyn NSDraggingInfo>,
) -> Bool {
let paths = unsafe { collect_paths(drag_info) };
let dl: NSPoint = unsafe { drag_info.draggingLocation() };
let frame: NSRect = this.frame();
let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
let listener = &this.ivars().drag_drop_handler;
if !listener(DragDropEvent::Drop { paths, position }) {
// Reject the Wry drop (invoke the OS default behaviour)
unsafe { objc2::msg_send![super(this), performDragOperation: drag_info] }
} else {
Bool::YES
}
}
pub(crate) fn dragging_exited(this: &WryWebView, drag_info: &ProtocolObject<dyn NSDraggingInfo>) {
let listener = &this.ivars().drag_drop_handler;
if !listener(DragDropEvent::Leave) {
// Reject the Wry drop (invoke the OS default behaviour)
unsafe { objc2::msg_send![super(this), draggingExited: drag_info] }
}
}

View File

@ -0,0 +1,625 @@
//! This file has been imported from `objc2-web-kit` and modified to match iOS.
#![allow(warnings)]
#![allow(clippy::all)]
use std::{ffi::c_double, ptr::NonNull};
use objc2::{
encode::{Encode, Encoding, RefEncode},
extern_class, extern_methods,
rc::{Allocated, Retained},
runtime::{AnyObject, ProtocolObject},
MainThreadOnly,
};
use objc2_core_foundation::*;
use objc2_foundation::*;
use objc2_ui_kit::*;
use objc2_web_kit::*;
use crate::*;
// NS_ENUM
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct WKMediaPlaybackState(pub NSInteger);
impl WKMediaPlaybackState {
#[doc(alias = "WKMediaPlaybackStateNone")]
pub const None: Self = Self(0);
#[doc(alias = "WKMediaPlaybackStatePlaying")]
pub const Playing: Self = Self(1);
#[doc(alias = "WKMediaPlaybackStatePaused")]
pub const Paused: Self = Self(2);
#[doc(alias = "WKMediaPlaybackStateSuspended")]
pub const Suspended: Self = Self(3);
}
unsafe impl Encode for WKMediaPlaybackState {
const ENCODING: Encoding = NSInteger::ENCODING;
}
unsafe impl RefEncode for WKMediaPlaybackState {
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
}
// NS_ENUM
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct WKMediaCaptureState(pub NSInteger);
impl WKMediaCaptureState {
#[doc(alias = "WKMediaCaptureStateNone")]
pub const None: Self = Self(0);
#[doc(alias = "WKMediaCaptureStateActive")]
pub const Active: Self = Self(1);
#[doc(alias = "WKMediaCaptureStateMuted")]
pub const Muted: Self = Self(2);
}
unsafe impl Encode for WKMediaCaptureState {
const ENCODING: Encoding = NSInteger::ENCODING;
}
unsafe impl RefEncode for WKMediaCaptureState {
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
}
// NS_ENUM
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct WKFullscreenState(pub NSInteger);
impl WKFullscreenState {
#[doc(alias = "WKFullscreenStateNotInFullscreen")]
pub const NotInFullscreen: Self = Self(0);
#[doc(alias = "WKFullscreenStateEnteringFullscreen")]
pub const EnteringFullscreen: Self = Self(1);
#[doc(alias = "WKFullscreenStateInFullscreen")]
pub const InFullscreen: Self = Self(2);
#[doc(alias = "WKFullscreenStateExitingFullscreen")]
pub const ExitingFullscreen: Self = Self(3);
}
unsafe impl Encode for WKFullscreenState {
const ENCODING: Encoding = NSInteger::ENCODING;
}
unsafe impl RefEncode for WKFullscreenState {
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
}
extern_class!(
#[unsafe(super(UIView, UIResponder, NSObject))]
#[thread_kind = MainThreadOnly]
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct WKWebView;
);
#[cfg(target_os = "macos")]
unsafe impl NSAccessibility for WKWebView {}
#[cfg(target_os = "macos")]
unsafe impl NSAccessibilityElementProtocol for WKWebView {}
#[cfg(target_os = "macos")]
unsafe impl NSAnimatablePropertyContainer for WKWebView {}
#[cfg(target_os = "macos")]
unsafe impl NSAppearanceCustomization for WKWebView {}
unsafe impl NSCoding for WKWebView {}
#[cfg(target_os = "macos")]
unsafe impl NSDraggingDestination for WKWebView {}
unsafe impl NSObjectProtocol for WKWebView {}
#[cfg(target_os = "macos")]
unsafe impl NSUserInterfaceItemIdentification for WKWebView {}
impl WKWebView {
extern_methods!(
// #[cfg(feature = "WKWebViewConfiguration")]
#[unsafe(method(configuration))]
pub unsafe fn configuration(&self) -> Retained<WKWebViewConfiguration>;
// #[cfg(feature = "WKNavigationDelegate")]
#[unsafe(method(navigationDelegate))]
pub unsafe fn navigationDelegate(
&self,
) -> Option<Retained<ProtocolObject<dyn WKNavigationDelegate>>>;
// #[cfg(feature = "WKNavigationDelegate")]
#[unsafe(method(setNavigationDelegate:))]
pub unsafe fn setNavigationDelegate(
&self,
navigation_delegate: Option<&ProtocolObject<dyn WKNavigationDelegate>>,
);
// #[cfg(feature = "WKUIDelegate")]
#[unsafe(method(UIDelegate))]
pub unsafe fn UIDelegate(&self) -> Option<Retained<ProtocolObject<dyn WKUIDelegate>>>;
// #[cfg(feature = "WKUIDelegate")]
#[unsafe(method(setUIDelegate:))]
pub unsafe fn setUIDelegate(&self, ui_delegate: Option<&ProtocolObject<dyn WKUIDelegate>>);
#[cfg(target_os = "macos")]
// #[cfg(feature = "WKBackForwardList")]
#[unsafe(method(backForwardList))]
pub unsafe fn backForwardList(&self) -> Retained<WKBackForwardList>;
// #[cfg(feature = "WKWebViewConfiguration")]
#[unsafe(method(initWithFrame:configuration:))]
pub unsafe fn initWithFrame_configuration(
this: Allocated<Self>,
frame: CGRect,
configuration: &WKWebViewConfiguration,
) -> Retained<Self>;
#[unsafe(method(initWithCoder:))]
pub unsafe fn initWithCoder(this: Allocated<Self>, coder: &NSCoder) -> Option<Retained<Self>>;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(loadRequest:))]
pub unsafe fn loadRequest(&self, request: &NSURLRequest) -> Option<Retained<WKNavigation>>;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(loadFileURL:allowingReadAccessToURL:))]
pub unsafe fn loadFileURL_allowingReadAccessToURL(
&self,
url: &NSURL,
read_access_url: &NSURL,
) -> Option<Retained<WKNavigation>>;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(loadHTMLString:baseURL:))]
pub unsafe fn loadHTMLString_baseURL(
&self,
string: &NSString,
base_url: Option<&NSURL>,
) -> Option<Retained<WKNavigation>>;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(loadData:MIMEType:characterEncodingName:baseURL:))]
pub unsafe fn loadData_MIMEType_characterEncodingName_baseURL(
&self,
data: &NSData,
mime_type: &NSString,
character_encoding_name: &NSString,
base_url: &NSURL,
) -> Option<Retained<WKNavigation>>;
#[cfg(target_os = "macos")]
// #[cfg(all(feature = "WKBackForwardListItem", feature = "WKNavigation"))]
#[unsafe(method(goToBackForwardListItem:))]
pub unsafe fn goToBackForwardListItem(
&self,
item: &WKBackForwardListItem,
) -> Option<Retained<WKNavigation>>;
#[unsafe(method(title))]
pub unsafe fn title(&self) -> Option<Retained<NSString>>;
#[unsafe(method(URL))]
pub unsafe fn URL(&self) -> Option<Retained<NSURL>>;
#[unsafe(method(isLoading))]
pub unsafe fn isLoading(&self) -> bool;
#[unsafe(method(estimatedProgress))]
pub unsafe fn estimatedProgress(&self) -> c_double;
#[unsafe(method(hasOnlySecureContent))]
pub unsafe fn hasOnlySecureContent(&self) -> bool;
#[unsafe(method(canGoBack))]
pub unsafe fn canGoBack(&self) -> bool;
#[unsafe(method(canGoForward))]
pub unsafe fn canGoForward(&self) -> bool;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(goBack))]
pub unsafe fn goBack(&self) -> Option<Retained<WKNavigation>>;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(goForward))]
pub unsafe fn goForward(&self) -> Option<Retained<WKNavigation>>;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(reload))]
pub unsafe fn reload(&self) -> Option<Retained<WKNavigation>>;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(reloadFromOrigin))]
pub unsafe fn reloadFromOrigin(&self) -> Option<Retained<WKNavigation>>;
#[unsafe(method(stopLoading))]
pub unsafe fn stopLoading(&self);
// #[cfg(feature = "block2")]
#[unsafe(method(evaluateJavaScript:completionHandler:))]
pub unsafe fn evaluateJavaScript_completionHandler(
&self,
java_script_string: &NSString,
completion_handler: Option<&block2::Block<dyn Fn(*mut AnyObject, *mut NSError)>>,
);
#[cfg(target_os = "macos")]
// #[cfg(all(
// feature = "WKContentWorld",
// feature = "WKFrameInfo",
// feature = "block2"
// ))]
#[unsafe(method(evaluateJavaScript:inFrame:inContentWorld:completionHandler:))]
pub unsafe fn evaluateJavaScript_inFrame_inContentWorld_completionHandler(
&self,
java_script_string: &NSString,
frame: Option<&WKFrameInfo>,
content_world: &WKContentWorld,
completion_handler: Option<&block2::Block<dyn Fn(*mut AnyObject, *mut NSError)>>,
);
#[cfg(target_os = "macos")]
// #[cfg(all(
// feature = "WKContentWorld",
// feature = "WKFrameInfo",
// feature = "block2"
// ))]
#[unsafe(method(callAsyncJavaScript:arguments:inFrame:inContentWorld:completionHandler:))]
pub unsafe fn callAsyncJavaScript_arguments_inFrame_inContentWorld_completionHandler(
&self,
function_body: &NSString,
arguments: Option<&NSDictionary<NSString, AnyObject>>,
frame: Option<&WKFrameInfo>,
content_world: &WKContentWorld,
completion_handler: Option<&block2::Block<dyn Fn(*mut AnyObject, *mut NSError)>>,
);
// #[cfg(feature = "block2")]
#[unsafe(method(closeAllMediaPresentationsWithCompletionHandler:))]
pub unsafe fn closeAllMediaPresentationsWithCompletionHandler(
&self,
completion_handler: Option<&block2::Block<dyn Fn()>>,
);
#[deprecated]
#[unsafe(method(closeAllMediaPresentations))]
pub unsafe fn closeAllMediaPresentations(&self);
// #[cfg(feature = "block2")]
#[unsafe(method(pauseAllMediaPlaybackWithCompletionHandler:))]
pub unsafe fn pauseAllMediaPlaybackWithCompletionHandler(
&self,
completion_handler: Option<&block2::Block<dyn Fn()>>,
);
// #[cfg(feature = "block2")]
#[deprecated]
#[unsafe(method(pauseAllMediaPlayback:))]
pub unsafe fn pauseAllMediaPlayback(
&self,
completion_handler: Option<&block2::Block<dyn Fn()>>,
);
// #[cfg(feature = "block2")]
#[unsafe(method(setAllMediaPlaybackSuspended:completionHandler:))]
pub unsafe fn setAllMediaPlaybackSuspended_completionHandler(
&self,
suspended: bool,
completion_handler: Option<&block2::Block<dyn Fn()>>,
);
// #[cfg(feature = "block2")]
#[deprecated]
#[unsafe(method(resumeAllMediaPlayback:))]
pub unsafe fn resumeAllMediaPlayback(
&self,
completion_handler: Option<&block2::Block<dyn Fn()>>,
);
// #[cfg(feature = "block2")]
#[deprecated]
#[unsafe(method(suspendAllMediaPlayback:))]
pub unsafe fn suspendAllMediaPlayback(
&self,
completion_handler: Option<&block2::Block<dyn Fn()>>,
);
// #[cfg(feature = "block2")]
#[unsafe(method(requestMediaPlaybackStateWithCompletionHandler:))]
pub unsafe fn requestMediaPlaybackStateWithCompletionHandler(
&self,
completion_handler: &block2::Block<dyn Fn(WKMediaPlaybackState)>,
);
// #[cfg(feature = "block2")]
#[deprecated]
#[unsafe(method(requestMediaPlaybackState:))]
pub unsafe fn requestMediaPlaybackState(
&self,
completion_handler: &block2::Block<dyn Fn(WKMediaPlaybackState)>,
);
#[unsafe(method(cameraCaptureState))]
pub unsafe fn cameraCaptureState(&self) -> WKMediaCaptureState;
#[unsafe(method(microphoneCaptureState))]
pub unsafe fn microphoneCaptureState(&self) -> WKMediaCaptureState;
// #[cfg(feature = "block2")]
#[unsafe(method(setCameraCaptureState:completionHandler:))]
pub unsafe fn setCameraCaptureState_completionHandler(
&self,
state: WKMediaCaptureState,
completion_handler: Option<&block2::Block<dyn Fn()>>,
);
// #[cfg(feature = "block2")]
#[unsafe(method(setMicrophoneCaptureState:completionHandler:))]
pub unsafe fn setMicrophoneCaptureState_completionHandler(
&self,
state: WKMediaCaptureState,
completion_handler: Option<&block2::Block<dyn Fn()>>,
);
#[cfg(target_os = "macos")]
// #[cfg(all(feature = "WKSnapshotConfiguration", feature = "block2"))]
#[unsafe(method(takeSnapshotWithConfiguration:completionHandler:))]
pub unsafe fn takeSnapshotWithConfiguration_completionHandler(
&self,
snapshot_configuration: Option<&WKSnapshotConfiguration>,
completion_handler: &block2::Block<dyn Fn(*mut NSImage, *mut NSError)>,
);
#[cfg(target_os = "macos")]
// #[cfg(all(feature = "WKPDFConfiguration", feature = "block2"))]
#[unsafe(method(createPDFWithConfiguration:completionHandler:))]
pub unsafe fn createPDFWithConfiguration_completionHandler(
&self,
pdf_configuration: Option<&WKPDFConfiguration>,
completion_handler: &block2::Block<dyn Fn(*mut NSData, *mut NSError)>,
);
// #[cfg(feature = "block2")]
#[unsafe(method(createWebArchiveDataWithCompletionHandler:))]
pub unsafe fn createWebArchiveDataWithCompletionHandler(
&self,
completion_handler: &block2::Block<dyn Fn(NonNull<NSData>, NonNull<NSError>)>,
);
#[unsafe(method(allowsBackForwardNavigationGestures))]
pub unsafe fn allowsBackForwardNavigationGestures(&self) -> bool;
#[unsafe(method(setAllowsBackForwardNavigationGestures:))]
pub unsafe fn setAllowsBackForwardNavigationGestures(
&self,
allows_back_forward_navigation_gestures: bool,
);
#[unsafe(method(customUserAgent))]
pub unsafe fn customUserAgent(&self) -> Option<Retained<NSString>>;
#[unsafe(method(setCustomUserAgent:))]
pub unsafe fn setCustomUserAgent(&self, custom_user_agent: Option<&NSString>);
#[unsafe(method(allowsLinkPreview))]
pub unsafe fn allowsLinkPreview(&self) -> bool;
#[unsafe(method(setAllowsLinkPreview:))]
pub unsafe fn setAllowsLinkPreview(&self, allows_link_preview: bool);
#[unsafe(method(allowsMagnification))]
pub unsafe fn allowsMagnification(&self) -> bool;
#[unsafe(method(setAllowsMagnification:))]
pub unsafe fn setAllowsMagnification(&self, allows_magnification: bool);
#[unsafe(method(magnification))]
pub unsafe fn magnification(&self) -> CGFloat;
#[unsafe(method(setMagnification:))]
pub unsafe fn setMagnification(&self, magnification: CGFloat);
#[unsafe(method(setMagnification:centeredAtPoint:))]
pub unsafe fn setMagnification_centeredAtPoint(&self, magnification: CGFloat, point: CGPoint);
#[unsafe(method(pageZoom))]
pub unsafe fn pageZoom(&self) -> CGFloat;
#[unsafe(method(setPageZoom:))]
pub unsafe fn setPageZoom(&self, page_zoom: CGFloat);
#[cfg(target_os = "macos")]
// #[cfg(all(
// feature = "WKFindConfiguration",
// feature = "WKFindResult",
// feature = "block2"
// ))]
#[unsafe(method(findString:withConfiguration:completionHandler:))]
pub unsafe fn findString_withConfiguration_completionHandler(
&self,
string: &NSString,
configuration: Option<&WKFindConfiguration>,
completion_handler: &block2::Block<dyn Fn(NonNull<WKFindResult>)>,
);
#[unsafe(method(handlesURLScheme:))]
pub unsafe fn handlesURLScheme(url_scheme: &NSString, mtm: MainThreadMarker) -> bool;
// #[cfg(all(feature = "WKDownload", feature = "block2"))]
#[unsafe(method(startDownloadUsingRequest:completionHandler:))]
pub unsafe fn startDownloadUsingRequest_completionHandler(
&self,
request: &NSURLRequest,
completion_handler: &block2::Block<dyn Fn(NonNull<WKDownload>)>,
);
// #[cfg(all(feature = "WKDownload", feature = "block2"))]
#[unsafe(method(resumeDownloadFromResumeData:completionHandler:))]
pub unsafe fn resumeDownloadFromResumeData_completionHandler(
&self,
resume_data: &NSData,
completion_handler: &block2::Block<dyn Fn(NonNull<WKDownload>)>,
);
#[unsafe(method(mediaType))]
pub unsafe fn mediaType(&self) -> Option<Retained<NSString>>;
#[unsafe(method(setMediaType:))]
pub unsafe fn setMediaType(&self, media_type: Option<&NSString>);
#[unsafe(method(interactionState))]
pub unsafe fn interactionState(&self) -> Option<Retained<AnyObject>>;
#[unsafe(method(setInteractionState:))]
pub unsafe fn setInteractionState(&self, interaction_state: Option<&AnyObject>);
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(loadSimulatedRequest:response:responseData:))]
pub unsafe fn loadSimulatedRequest_response_responseData(
&self,
request: &NSURLRequest,
response: &NSURLResponse,
data: &NSData,
) -> Retained<WKNavigation>;
// #[cfg(feature = "WKNavigation")]
#[deprecated]
#[unsafe(method(loadSimulatedRequest:withResponse:responseData:))]
pub unsafe fn loadSimulatedRequest_withResponse_responseData(
&self,
request: &NSURLRequest,
response: &NSURLResponse,
data: &NSData,
) -> Retained<WKNavigation>;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(loadFileRequest:allowingReadAccessToURL:))]
pub unsafe fn loadFileRequest_allowingReadAccessToURL(
&self,
request: &NSURLRequest,
read_access_url: &NSURL,
) -> Retained<WKNavigation>;
// #[cfg(feature = "WKNavigation")]
#[unsafe(method(loadSimulatedRequest:responseHTMLString:))]
pub unsafe fn loadSimulatedRequest_responseHTMLString(
&self,
request: &NSURLRequest,
string: &NSString,
) -> Retained<WKNavigation>;
// #[cfg(feature = "WKNavigation")]
#[deprecated]
#[unsafe(method(loadSimulatedRequest:withResponseHTMLString:))]
pub unsafe fn loadSimulatedRequest_withResponseHTMLString(
&self,
request: &NSURLRequest,
string: &NSString,
) -> Retained<WKNavigation>;
#[cfg(target_os = "macos")]
#[unsafe(method(printOperationWithPrintInfo:))]
pub unsafe fn printOperationWithPrintInfo(
&self,
print_info: &NSPrintInfo,
) -> Retained<NSPrintOperation>;
#[cfg(target_os = "macos")]
#[unsafe(method(themeColor))]
pub unsafe fn themeColor(&self) -> Option<Retained<NSColor>>;
#[cfg(target_os = "macos")]
#[unsafe(method(underPageBackgroundColor))]
pub unsafe fn underPageBackgroundColor(&self) -> Retained<NSColor>;
#[cfg(target_os = "macos")]
#[unsafe(method(setUnderPageBackgroundColor:))]
pub unsafe fn setUnderPageBackgroundColor(&self, under_page_background_color: Option<&NSColor>);
#[unsafe(method(fullscreenState))]
pub unsafe fn fullscreenState(&self) -> WKFullscreenState;
#[unsafe(method(minimumViewportInset))]
pub unsafe fn minimumViewportInset(&self) -> NSEdgeInsets;
#[unsafe(method(maximumViewportInset))]
pub unsafe fn maximumViewportInset(&self) -> NSEdgeInsets;
#[unsafe(method(setMinimumViewportInset:maximumViewportInset:))]
pub unsafe fn setMinimumViewportInset_maximumViewportInset(
&self,
minimum_viewport_inset: NSEdgeInsets,
maximum_viewport_inset: NSEdgeInsets,
);
#[unsafe(method(isInspectable))]
pub unsafe fn isInspectable(&self) -> bool;
#[unsafe(method(setInspectable:))]
pub unsafe fn setInspectable(&self, inspectable: bool);
);
}
/// Methods declared on superclass `UIView`
impl WKWebView {
extern_methods!(
#[unsafe(method(initWithFrame:))]
pub unsafe fn initWithFrame(this: Allocated<Self>, frame_rect: NSRect) -> Retained<Self>;
);
}
/// Methods declared on superclass `UIResponder`
impl WKWebView {
extern_methods!(
#[unsafe(method(init))]
pub unsafe fn init(this: Allocated<Self>) -> Retained<Self>;
);
}
/// Methods declared on superclass `NSObject`
impl WKWebView {
extern_methods!(
#[unsafe(method(new))]
pub unsafe fn new(mtm: MainThreadMarker) -> Retained<Self>;
);
}
/// WKIBActions
impl WKWebView {
extern_methods!(
#[unsafe(method(goBack:))]
pub unsafe fn goBack_(&self, sender: Option<&AnyObject>);
#[unsafe(method(goForward:))]
pub unsafe fn goForward_(&self, sender: Option<&AnyObject>);
#[unsafe(method(reload:))]
pub unsafe fn reload_(&self, sender: Option<&AnyObject>);
#[unsafe(method(reloadFromOrigin:))]
pub unsafe fn reloadFromOrigin_(&self, sender: Option<&AnyObject>);
#[unsafe(method(stopLoading:))]
pub unsafe fn stopLoading_(&self, sender: Option<&AnyObject>);
);
}
#[cfg(target_os = "macos")]
unsafe impl NSUserInterfaceValidations for WKWebView {}
/// WKNSTextFinderClient
impl WKWebView {
extern_methods!();
}
#[cfg(target_os = "macos")]
unsafe impl NSTextFinderClient for WKWebView {}
/// WKDeprecated
impl WKWebView {
extern_methods!(
#[deprecated]
#[unsafe(method(certificateChain))]
pub unsafe fn certificateChain(&self) -> Retained<NSArray>;
);
}

1
vendor/wry/src/wkwebview/ios/mod.rs vendored Normal file
View File

@ -0,0 +1 @@
pub mod WKWebView;

1472
vendor/wry/src/wkwebview/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

116
vendor/wry/src/wkwebview/navigation.rs vendored Normal file
View File

@ -0,0 +1,116 @@
use objc2::DeclaredClass;
use objc2_foundation::{NSObjectProtocol, NSString};
use objc2_web_kit::{
WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationResponse,
WKNavigationResponsePolicy,
};
#[cfg(target_os = "ios")]
use crate::wkwebview::ios::WKWebView::WKWebView;
#[cfg(target_os = "macos")]
use objc2_web_kit::WKWebView;
use crate::PageLoadEvent;
use super::class::wry_navigation_delegate::WryNavigationDelegate;
pub(crate) fn did_commit_navigation(
this: &WryNavigationDelegate,
webview: &WKWebView,
_navigation: &WKNavigation,
) {
unsafe {
// Call on_load_handler
if let Some(on_page_load) = &this.ivars().on_page_load_handler {
on_page_load(PageLoadEvent::Started);
}
// Inject scripts
let mut pending_scripts = this.ivars().pending_scripts.lock().unwrap();
if let Some(scripts) = &*pending_scripts {
for script in scripts {
webview.evaluateJavaScript_completionHandler(&NSString::from_str(script), None);
}
*pending_scripts = None;
}
}
}
pub(crate) fn did_finish_navigation(
this: &WryNavigationDelegate,
_webview: &WKWebView,
_navigation: &WKNavigation,
) {
if let Some(on_page_load) = &this.ivars().on_page_load_handler {
on_page_load(PageLoadEvent::Finished);
}
}
// Navigation handler
pub(crate) fn navigation_policy(
this: &WryNavigationDelegate,
_webview: &WKWebView,
action: &WKNavigationAction,
handler: &block2::Block<dyn Fn(WKNavigationActionPolicy)>,
) {
unsafe {
// <https://developer.apple.com/documentation/webkit/wknavigationaction/shouldperformdownload>
// Available: macOS 11.3+, iOS 14.5+
let can_download = action.respondsToSelector(objc2::sel!(shouldPerformDownload));
let should_download: bool = if can_download {
action.shouldPerformDownload()
} else {
false
};
let request = action.request();
let url = request.URL().unwrap().absoluteString().unwrap();
if should_download {
let has_download_handler = this.ivars().has_download_handler;
if has_download_handler {
(*handler).call((WKNavigationActionPolicy::Download,));
} else {
(*handler).call((WKNavigationActionPolicy::Cancel,));
}
} else {
let function = &this.ivars().navigation_policy_function;
match function(url.to_string()) {
true => (*handler).call((WKNavigationActionPolicy::Allow,)),
false => (*handler).call((WKNavigationActionPolicy::Cancel,)),
};
}
}
}
// Navigation handler
pub(crate) fn navigation_policy_response(
this: &WryNavigationDelegate,
_webview: &WKWebView,
response: &WKNavigationResponse,
handler: &block2::Block<dyn Fn(WKNavigationResponsePolicy)>,
) {
unsafe {
let can_show_mime_type = response.canShowMIMEType();
if !can_show_mime_type {
let has_download_handler = this.ivars().has_download_handler;
if has_download_handler {
(*handler).call((WKNavigationResponsePolicy::Download,));
return;
}
}
(*handler).call((WKNavigationResponsePolicy::Allow,));
}
}
pub(crate) fn web_content_process_did_terminate(
this: &WryNavigationDelegate,
_webview: &WKWebView,
) {
if let Some(on_web_content_process_terminate) =
&this.ivars().on_web_content_process_terminate_handler
{
on_web_content_process_terminate();
}
}

44
vendor/wry/src/wkwebview/proxy.rs vendored Normal file
View File

@ -0,0 +1,44 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use objc2_foundation::NSObject;
use std::ffi::{c_char, CString};
use crate::{proxy::ProxyEndpoint, Error};
#[allow(non_camel_case_types)]
pub type nw_endpoint_t = *mut NSObject;
#[allow(non_camel_case_types)]
pub type nw_protocol_options_t = *mut NSObject;
#[allow(non_camel_case_types)]
pub type nw_proxy_config_t = *mut NSObject;
#[link(name = "Network", kind = "framework")]
extern "C" {
fn nw_endpoint_create_host(host: *const c_char, port: *const c_char) -> nw_endpoint_t;
pub fn nw_proxy_config_create_socksv5(proxy_endpoint: nw_endpoint_t) -> nw_proxy_config_t;
pub fn nw_proxy_config_create_http_connect(
proxy_endpoint: nw_endpoint_t,
proxy_tls_options: nw_protocol_options_t,
) -> nw_proxy_config_t;
}
impl TryFrom<ProxyEndpoint> for nw_endpoint_t {
type Error = Error;
fn try_from(endpoint: ProxyEndpoint) -> Result<Self, Error> {
unsafe {
let endpoint_host =
CString::new(endpoint.host).map_err(|_| Error::ProxyEndpointCreationFailed)?;
let endpoint_port =
CString::new(endpoint.port).map_err(|_| Error::ProxyEndpointCreationFailed)?;
let endpoint = nw_endpoint_create_host(endpoint_host.as_ptr(), endpoint_port.as_ptr());
if endpoint.is_null() {
Err(Error::ProxyEndpointCreationFailed)
} else {
Ok(endpoint)
}
}
}
}

View File

@ -0,0 +1,123 @@
use objc2_app_kit::{
NSAlternateKeyMask, NSCommandKeyMask, NSControlKeyMask, NSEvent, NSEventType, NSShiftKeyMask,
NSView,
};
use objc2_foundation::NSString;
use super::WryWebView;
pub(crate) fn other_mouse_down(this: &WryWebView, event: &NSEvent) {
unsafe {
if event.r#type() == NSEventType::OtherMouseDown {
let button_number = event.buttonNumber();
match button_number {
// back button
3 => {
let js = create_js_mouse_event(this, event, true, true);
this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
return;
}
// forward button
4 => {
let js = create_js_mouse_event(this, event, true, false);
this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
return;
}
_ => {}
}
}
this.mouseDown(event);
}
}
pub(crate) fn other_mouse_up(this: &WryWebView, event: &NSEvent) {
unsafe {
if event.r#type() == NSEventType::OtherMouseUp {
let button_number = event.buttonNumber();
match button_number {
// back button
3 => {
let js = create_js_mouse_event(this, event, false, true);
this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
return;
}
// forward button
4 => {
let js = create_js_mouse_event(this, event, false, false);
this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
return;
}
_ => {}
}
}
this.mouseUp(event);
}
}
unsafe fn create_js_mouse_event(
view: &NSView,
event: &NSEvent,
down: bool,
back_button: bool,
) -> String {
let event_name = if down { "mousedown" } else { "mouseup" };
// js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
let button = if back_button { 3 } else { 4 };
let mods_flags = event.modifierFlags();
let window_point = event.locationInWindow();
let view_point = view.convertPoint_fromView(window_point, None);
let x = view_point.x as u32;
let y = view_point.y as u32;
// js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
let buttons = NSEvent::pressedMouseButtons();
format!(
r#"(() => {{
const el = document.elementFromPoint({x},{y});
const ev = new MouseEvent('{event_name}', {{
view: window,
button: {button},
buttons: {buttons},
x: {x},
y: {y},
bubbles: true,
detail: {detail},
cancelBubble: false,
cancelable: true,
clientX: {x},
clientY: {y},
composed: true,
layerX: {x},
layerY: {y},
pageX: {x},
pageY: {y},
screenX: window.screenX + {x},
screenY: window.screenY + {y},
ctrlKey: {ctrl_key},
metaKey: {meta_key},
shiftKey: {shift_key},
altKey: {alt_key},
}});
el.dispatchEvent(ev)
if (!ev.defaultPrevented && "{event_name}" === "mouseup") {{
if (ev.button === 3) {{
window.history.back();
}}
if (ev.button === 4) {{
window.history.forward();
}}
}}
}})()"#,
event_name = event_name,
x = x,
y = y,
detail = event.clickCount(),
ctrl_key = mods_flags.contains(NSControlKeyMask),
alt_key = mods_flags.contains(NSAlternateKeyMask),
shift_key = mods_flags.contains(NSShiftKeyMask),
meta_key = mods_flags.contains(NSCommandKeyMask),
button = button,
buttons = buttons,
)
}

15
vendor/wry/src/wkwebview/util.rs vendored Normal file
View File

@ -0,0 +1,15 @@
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use objc2_foundation::NSProcessInfo;
pub fn operating_system_version() -> (isize, isize, isize) {
let process_info = NSProcessInfo::processInfo();
let version = process_info.operatingSystemVersion();
(
version.majorVersion,
version.minorVersion,
version.patchVersion,
)
}