From e82c1aedf0bce0081f3d9ff317dd1df908bda857 Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 5 Aug 2026 00:07:41 +0800 Subject: [PATCH] fix(windows): restore Windows 7 startup compatibility --- .../scripts/assert-webview2-win7-runtime.ps1 | 56 + .../scripts/assert-win7-installer-content.ps1 | 38 + .github/scripts/assert-win7-pe-compat.ps1 | 71 + .../scripts/prepare-webview2-win7-loader.ps1 | 106 + .../scripts/prepare-webview2-win7-runtime.ps1 | 81 +- .github/workflows/ci.yml | 70 +- .github/workflows/release.yml | 32 +- .gitignore | 1 + Cargo.lock | 8 +- Cargo.toml | 10 +- .../windowsInstallerTemplate.spec.ts | 80 + crates/dbx-core/Cargo.toml | 4 +- crates/dbx-core/src/db/postgres.rs | 24 +- src-tauri/Cargo.toml | 1 + src-tauri/build.rs | 5 + ...on => tauri.webview2-win7-fixed.conf.json} | 4 +- vendor/dirs-sys/Cargo.toml | 56 + vendor/dirs-sys/DBX-PATCH.md | 6 + vendor/dirs-sys/LICENSE-APACHE | 174 ++ vendor/dirs-sys/LICENSE-MIT | 19 + vendor/dirs-sys/README.md | 49 + vendor/dirs-sys/src/lib.rs | 233 ++ vendor/dirs-sys/src/xdg_user_dirs.rs | 248 ++ vendor/pageant/Cargo.toml | 108 + vendor/pageant/DBX-PATCH.md | 6 + vendor/pageant/src/error.rs | 44 + vendor/pageant/src/interface.rs | 113 + vendor/pageant/src/lib.rs | 32 + vendor/pageant/src/namedpipes.rs | 154 + vendor/pageant/src/wmmessage.rs | 291 ++ vendor/wry/Cargo.toml | 424 +++ vendor/wry/DBX-PATCH.md | 9 + vendor/wry/LICENSE-APACHE | 201 ++ vendor/wry/LICENSE-MIT | 21 + vendor/wry/LICENSE.spdx | 20 + vendor/wry/README.md | 317 +++ vendor/wry/build.rs | 118 + vendor/wry/src/android/binding.rs | 483 ++++ vendor/wry/src/android/kotlin/Ipc.kt | 23 + vendor/wry/src/android/kotlin/Logger.kt | 87 + .../src/android/kotlin/PermissionHelper.kt | 115 + vendor/wry/src/android/kotlin/Rust.kt | 46 + .../src/android/kotlin/RustWebChromeClient.kt | 491 ++++ vendor/wry/src/android/kotlin/RustWebView.kt | 96 + .../src/android/kotlin/RustWebViewClient.kt | 98 + vendor/wry/src/android/kotlin/WryActivity.kt | 173 ++ .../wry/src/android/kotlin/proguard-wry.pro | 35 + vendor/wry/src/android/main_pipe.rs | 607 ++++ vendor/wry/src/android/mod.rs | 530 ++++ vendor/wry/src/custom_protocol_workaround.rs | 56 + vendor/wry/src/error.rs | 80 + .../wry/src/inject_initialization_scripts.rs | 225 ++ vendor/wry/src/lib.rs | 2514 +++++++++++++++++ vendor/wry/src/proxy.rs | 15 + vendor/wry/src/util.rs | 13 + vendor/wry/src/web_context.rs | 113 + vendor/wry/src/webkitgtk/drag_drop.rs | 143 + vendor/wry/src/webkitgtk/mod.rs | 1249 ++++++++ .../src/webkitgtk/synthetic_mouse_events.rs | 187 ++ vendor/wry/src/webkitgtk/web_context.rs | 404 +++ vendor/wry/src/webview2/drag_drop.rs | 251 ++ vendor/wry/src/webview2/mod.rs | 1909 +++++++++++++ vendor/wry/src/webview2/util.rs | 101 + .../class/document_title_changed_observer.rs | 90 + vendor/wry/src/wkwebview/class/mod.rs | 12 + .../src/wkwebview/class/url_scheme_handler.rs | 346 +++ .../wkwebview/class/wry_download_delegate.rs | 67 + .../class/wry_navigation_delegate.rs | 158 ++ .../wry/src/wkwebview/class/wry_web_view.rs | 153 + .../wkwebview/class/wry_web_view_delegate.rs | 105 + .../wkwebview/class/wry_web_view_parent.rs | 110 + .../class/wry_web_view_ui_delegate.rs | 282 ++ vendor/wry/src/wkwebview/download.rs | 122 + vendor/wry/src/wkwebview/drag_drop.rs | 104 + vendor/wry/src/wkwebview/ios/WKWebView.rs | 625 ++++ vendor/wry/src/wkwebview/ios/mod.rs | 1 + vendor/wry/src/wkwebview/mod.rs | 1472 ++++++++++ vendor/wry/src/wkwebview/navigation.rs | 116 + vendor/wry/src/wkwebview/proxy.rs | 44 + .../src/wkwebview/synthetic_mouse_events.rs | 123 + vendor/wry/src/wkwebview/util.rs | 15 + 81 files changed, 17157 insertions(+), 66 deletions(-) create mode 100644 .github/scripts/assert-webview2-win7-runtime.ps1 create mode 100644 .github/scripts/assert-win7-installer-content.ps1 create mode 100644 .github/scripts/assert-win7-pe-compat.ps1 create mode 100644 .github/scripts/prepare-webview2-win7-loader.ps1 rename src-tauri/{tauri.webview2-win7-offline.conf.json => tauri.webview2-win7-fixed.conf.json} (62%) create mode 100644 vendor/dirs-sys/Cargo.toml create mode 100644 vendor/dirs-sys/DBX-PATCH.md create mode 100644 vendor/dirs-sys/LICENSE-APACHE create mode 100644 vendor/dirs-sys/LICENSE-MIT create mode 100644 vendor/dirs-sys/README.md create mode 100644 vendor/dirs-sys/src/lib.rs create mode 100644 vendor/dirs-sys/src/xdg_user_dirs.rs create mode 100644 vendor/pageant/Cargo.toml create mode 100644 vendor/pageant/DBX-PATCH.md create mode 100644 vendor/pageant/src/error.rs create mode 100644 vendor/pageant/src/interface.rs create mode 100644 vendor/pageant/src/lib.rs create mode 100644 vendor/pageant/src/namedpipes.rs create mode 100644 vendor/pageant/src/wmmessage.rs create mode 100644 vendor/wry/Cargo.toml create mode 100644 vendor/wry/DBX-PATCH.md create mode 100644 vendor/wry/LICENSE-APACHE create mode 100644 vendor/wry/LICENSE-MIT create mode 100644 vendor/wry/LICENSE.spdx create mode 100644 vendor/wry/README.md create mode 100644 vendor/wry/build.rs create mode 100644 vendor/wry/src/android/binding.rs create mode 100644 vendor/wry/src/android/kotlin/Ipc.kt create mode 100644 vendor/wry/src/android/kotlin/Logger.kt create mode 100644 vendor/wry/src/android/kotlin/PermissionHelper.kt create mode 100644 vendor/wry/src/android/kotlin/Rust.kt create mode 100644 vendor/wry/src/android/kotlin/RustWebChromeClient.kt create mode 100644 vendor/wry/src/android/kotlin/RustWebView.kt create mode 100644 vendor/wry/src/android/kotlin/RustWebViewClient.kt create mode 100644 vendor/wry/src/android/kotlin/WryActivity.kt create mode 100644 vendor/wry/src/android/kotlin/proguard-wry.pro create mode 100644 vendor/wry/src/android/main_pipe.rs create mode 100644 vendor/wry/src/android/mod.rs create mode 100644 vendor/wry/src/custom_protocol_workaround.rs create mode 100644 vendor/wry/src/error.rs create mode 100644 vendor/wry/src/inject_initialization_scripts.rs create mode 100644 vendor/wry/src/lib.rs create mode 100644 vendor/wry/src/proxy.rs create mode 100644 vendor/wry/src/util.rs create mode 100644 vendor/wry/src/web_context.rs create mode 100644 vendor/wry/src/webkitgtk/drag_drop.rs create mode 100644 vendor/wry/src/webkitgtk/mod.rs create mode 100644 vendor/wry/src/webkitgtk/synthetic_mouse_events.rs create mode 100644 vendor/wry/src/webkitgtk/web_context.rs create mode 100644 vendor/wry/src/webview2/drag_drop.rs create mode 100644 vendor/wry/src/webview2/mod.rs create mode 100644 vendor/wry/src/webview2/util.rs create mode 100644 vendor/wry/src/wkwebview/class/document_title_changed_observer.rs create mode 100644 vendor/wry/src/wkwebview/class/mod.rs create mode 100644 vendor/wry/src/wkwebview/class/url_scheme_handler.rs create mode 100644 vendor/wry/src/wkwebview/class/wry_download_delegate.rs create mode 100644 vendor/wry/src/wkwebview/class/wry_navigation_delegate.rs create mode 100644 vendor/wry/src/wkwebview/class/wry_web_view.rs create mode 100644 vendor/wry/src/wkwebview/class/wry_web_view_delegate.rs create mode 100644 vendor/wry/src/wkwebview/class/wry_web_view_parent.rs create mode 100644 vendor/wry/src/wkwebview/class/wry_web_view_ui_delegate.rs create mode 100644 vendor/wry/src/wkwebview/download.rs create mode 100644 vendor/wry/src/wkwebview/drag_drop.rs create mode 100644 vendor/wry/src/wkwebview/ios/WKWebView.rs create mode 100644 vendor/wry/src/wkwebview/ios/mod.rs create mode 100644 vendor/wry/src/wkwebview/mod.rs create mode 100644 vendor/wry/src/wkwebview/navigation.rs create mode 100644 vendor/wry/src/wkwebview/proxy.rs create mode 100644 vendor/wry/src/wkwebview/synthetic_mouse_events.rs create mode 100644 vendor/wry/src/wkwebview/util.rs diff --git a/.github/scripts/assert-webview2-win7-runtime.ps1 b/.github/scripts/assert-webview2-win7-runtime.ps1 new file mode 100644 index 000000000..cef9f4f04 --- /dev/null +++ b/.github/scripts/assert-webview2-win7-runtime.ps1 @@ -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" diff --git a/.github/scripts/assert-win7-installer-content.ps1 b/.github/scripts/assert-win7-installer-content.ps1 new file mode 100644 index 000000000..8e90cd8da --- /dev/null +++ b/.github/scripts/assert-win7-installer-content.ps1 @@ -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)." +} diff --git a/.github/scripts/assert-win7-pe-compat.ps1 b/.github/scripts/assert-win7-pe-compat.ps1 new file mode 100644 index 000000000..84438f9f1 --- /dev/null +++ b/.github/scripts/assert-win7-pe-compat.ps1 @@ -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" diff --git a/.github/scripts/prepare-webview2-win7-loader.ps1 b/.github/scripts/prepare-webview2-win7-loader.ps1 new file mode 100644 index 000000000..723fe1f64 --- /dev/null +++ b/.github/scripts/prepare-webview2-win7-loader.ps1 @@ -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 + } +} diff --git a/.github/scripts/prepare-webview2-win7-runtime.ps1 b/.github/scripts/prepare-webview2-win7-runtime.ps1 index ed522bb49..15e41f150 100644 --- a/.github/scripts/prepare-webview2-win7-runtime.ps1 +++ b/.github/scripts/prepare-webview2-win7-runtime.ps1 @@ -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/(?[^/]+)/(?[^/?]+)" -) -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" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a63738e42..00593a079 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6cb047cca..dfd8b6568 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.gitignore b/.gitignore index b5c82134d..6a9321331 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 14987fa8d..a6252d5df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 0b37b4681..b03f21d49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/apps/desktop/src/lib/__tests__/windowsInstallerTemplate.spec.ts b/apps/desktop/src/lib/__tests__/windowsInstallerTemplate.spec.ts index 6eb32b5bb..605b96b20 100644 --- a/apps/desktop/src/lib/__tests__/windowsInstallerTemplate.spec.ts +++ b/apps/desktop/src/lib/__tests__/windowsInstallerTemplate.spec.ts @@ -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"); + }); +}); diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index de32b57df..a91d12702 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -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" diff --git a/crates/dbx-core/src/db/postgres.rs b/crates/dbx-core/src/db/postgres.rs index 2411d47d1..7de45fc74 100644 --- a/crates/dbx-core/src/db/postgres.rs +++ b/crates/dbx-core/src/db/postgres.rs @@ -1441,11 +1441,27 @@ async fn stream_query_rows_text_on_client( } pub async fn connect(url: &str, fallback_timeout: Duration) -> Result { - 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 { + 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 { 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) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1a27aab15..1c1f1bf10 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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"] diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 7631374e0..dde8e312e 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -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() } diff --git a/src-tauri/tauri.webview2-win7-offline.conf.json b/src-tauri/tauri.webview2-win7-fixed.conf.json similarity index 62% rename from src-tauri/tauri.webview2-win7-offline.conf.json rename to src-tauri/tauri.webview2-win7-fixed.conf.json index 80c27a370..5cc005a23 100644 --- a/src-tauri/tauri.webview2-win7-offline.conf.json +++ b/src-tauri/tauri.webview2-win7-fixed.conf.json @@ -3,8 +3,8 @@ "createUpdaterArtifacts": false, "windows": { "webviewInstallMode": { - "silent": true, - "type": "offlineInstaller" + "type": "fixedRuntime", + "path": "webview2-fixed-runtime" } } } diff --git a/vendor/dirs-sys/Cargo.toml b/vendor/dirs-sys/Cargo.toml new file mode 100644 index 000000000..a5032ae37 --- /dev/null +++ b/vendor/dirs-sys/Cargo.toml @@ -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 "] +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", +] diff --git a/vendor/dirs-sys/DBX-PATCH.md b/vendor/dirs-sys/DBX-PATCH.md new file mode 100644 index 000000000..3d2a37ae4 --- /dev/null +++ b/vendor/dirs-sys/DBX-PATCH.md @@ -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. diff --git a/vendor/dirs-sys/LICENSE-APACHE b/vendor/dirs-sys/LICENSE-APACHE new file mode 100644 index 000000000..91e18a62b --- /dev/null +++ b/vendor/dirs-sys/LICENSE-APACHE @@ -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. diff --git a/vendor/dirs-sys/LICENSE-MIT b/vendor/dirs-sys/LICENSE-MIT new file mode 100644 index 000000000..1452dc143 --- /dev/null +++ b/vendor/dirs-sys/LICENSE-MIT @@ -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. diff --git a/vendor/dirs-sys/README.md b/vendor/dirs-sys/README.md new file mode 100644 index 000000000..ea475e525 --- /dev/null +++ b/vendor/dirs-sys/README.md @@ -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. diff --git a/vendor/dirs-sys/src/lib.rs b/vendor/dirs-sys/src/lib.rs new file mode 100644 index 000000000..ce3bb1728 --- /dev/null +++ b/vendor/dirs-sys/src/lib.rs @@ -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 { + 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 { + 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 { + None + } + #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "emscripten")))] + unsafe fn fallback() -> Option { + 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 { + 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 { + 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 { + 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 { + 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 { + known_folder(Shell::FOLDERID_Profile) +} + +pub fn known_folder_roaming_app_data() -> Option { + known_folder(Shell::FOLDERID_RoamingAppData) +} + +pub fn known_folder_local_app_data() -> Option { + known_folder(Shell::FOLDERID_LocalAppData) +} + +pub fn known_folder_music() -> Option { + known_folder(Shell::FOLDERID_Music) +} + +pub fn known_folder_desktop() -> Option { + known_folder(Shell::FOLDERID_Desktop) +} + +pub fn known_folder_documents() -> Option { + known_folder(Shell::FOLDERID_Documents) +} + +pub fn known_folder_downloads() -> Option { + known_folder(Shell::FOLDERID_Downloads) +} + +pub fn known_folder_pictures() -> Option { + known_folder(Shell::FOLDERID_Pictures) +} + +pub fn known_folder_public() -> Option { + known_folder(Shell::FOLDERID_Public) +} +pub fn known_folder_templates() -> Option { + known_folder(Shell::FOLDERID_Templates) +} +pub fn known_folder_videos() -> Option { + 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 +}; diff --git a/vendor/dirs-sys/src/xdg_user_dirs.rs b/vendor/dirs-sys/src/xdg_user_dirs.rs new file mode 100644 index 000000000..82e90189c --- /dev/null +++ b/vendor/dirs-sys/src/xdg_user_dirs.rs @@ -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 { + 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 { + 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 { + 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> { + 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 characters removed. +fn trim_blank(bytes: &[u8]) -> &[u8] { + // Trim leading characters. + let i = bytes.iter().cloned().take_while(|b| *b == b' ' || *b == b'\t').count(); + let bytes = &bytes[i..]; + + // Trim trailing 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 { + // 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 . + + let mut unescaped: Vec = 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 = 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 = 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 = 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 = 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)); + } +} diff --git a/vendor/pageant/Cargo.toml b/vendor/pageant/Cargo.toml new file mode 100644 index 000000000..b5cc3e42b --- /dev/null +++ b/vendor/pageant/Cargo.toml @@ -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 "] +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" diff --git a/vendor/pageant/DBX-PATCH.md b/vendor/pageant/DBX-PATCH.md new file mode 100644 index 000000000..7b5caa59c --- /dev/null +++ b/vendor/pageant/DBX-PATCH.md @@ -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. diff --git a/vendor/pageant/src/error.rs b/vendor/pageant/src/error.rs new file mode 100644 index 000000000..7ec683298 --- /dev/null +++ b/vendor/pageant/src/error.rs @@ -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), +} + +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) + } +} diff --git a/vendor/pageant/src/interface.rs b/vendor/pageant/src/interface.rs new file mode 100644 index 000000000..ebb62c2ac --- /dev/null +++ b/vendor/pageant/src/interface.rs @@ -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 { + 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> { + 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> { + 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> { + 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> { + 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), + } + } +} diff --git a/vendor/pageant/src/lib.rs b/vendor/pageant/src/lib.rs new file mode 100644 index 000000000..7c1bd8450 --- /dev/null +++ b/vendor/pageant/src/lib.rs @@ -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::*; diff --git a/vendor/pageant/src/namedpipes.rs b/vendor/pageant/src/namedpipes.rs new file mode 100644 index 000000000..81eec49f4 --- /dev/null +++ b/vendor/pageant/src/namedpipes.rs @@ -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 { + 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 { + let username = Self::get_username()?; + let suffix = Self::capi_obfuscate_string("Pageant")?; + Ok(format!("\\\\.\\pipe\\pageant.{username}.{suffix}")) + } + + fn get_username() -> Result { + 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 { + 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>; + + } + } +} + +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>; + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>; + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll>; + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>; + } + + to Pin::new(&self.stream) { + fn is_write_vectored(&self) -> bool; + } + } +} diff --git a/vendor/pageant/src/wmmessage.rs b/vendor/pageant/src/wmmessage.rs new file mode 100644 index 000000000..14ce0c5ee --- /dev/null +++ b/vendor/pageant/src/wmmessage.rs @@ -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 { + let (one, mut two) = tokio::io::duplex(_AGENT_MAX_MSGLEN * 100); + + let cookie = rand::random::().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>; + + } + } +} + +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>; + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>; + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll>; + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>; + } + + 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, + ) -> Result { + 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 { + 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 { + 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 { + 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 { + 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, 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::() 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) +} diff --git a/vendor/wry/Cargo.toml b/vendor/wry/Cargo.toml new file mode 100644 index 000000000..0988872b0 --- /dev/null +++ b/vendor/wry/Cargo.toml @@ -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)", +] diff --git a/vendor/wry/DBX-PATCH.md b/vendor/wry/DBX-PATCH.md new file mode 100644 index 000000000..634b46755 --- /dev/null +++ b/vendor/wry/DBX-PATCH.md @@ -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. diff --git a/vendor/wry/LICENSE-APACHE b/vendor/wry/LICENSE-APACHE new file mode 100644 index 000000000..16fe87b06 --- /dev/null +++ b/vendor/wry/LICENSE-APACHE @@ -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. diff --git a/vendor/wry/LICENSE-MIT b/vendor/wry/LICENSE-MIT new file mode 100644 index 000000000..94ef8bf76 --- /dev/null +++ b/vendor/wry/LICENSE-MIT @@ -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. diff --git a/vendor/wry/LICENSE.spdx b/vendor/wry/LICENSE.spdx new file mode 100644 index 000000000..7e0eb72a9 --- /dev/null +++ b/vendor/wry/LICENSE.spdx @@ -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: Wry is the official, rust-based webview +windowing service for Tauri. + +PackageComment: The package includes the following libraries; see +Relationship information. + +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 \ No newline at end of file diff --git a/vendor/wry/README.md b/vendor/wry/README.md new file mode 100644 index 000000000..495176fee --- /dev/null +++ b/vendor/wry/README.md @@ -0,0 +1,317 @@ +

WRY Webview Rendering library

+ +[![](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, + webview: Option, +} + +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, + webview: Option, +} + +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